diff --git a/apps/sim/app/api/chat/manage/[id]/route.test.ts b/apps/sim/app/api/chat/manage/[id]/route.test.ts index 5808f2cbb58..63fad11f3a6 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.test.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.test.ts @@ -89,6 +89,7 @@ vi.mock('@/lib/workflows/persistence/utils', () => ({ })) vi.mock('@/lib/workflows/orchestration', () => ({ performChatUndeploy: mockPerformChatUndeploy, + notifySocketDeploymentChanged: vi.fn().mockResolvedValue(undefined), })) vi.mock('drizzle-orm', () => ({ and: vi.fn((...conditions: unknown[]) => ({ type: 'and', conditions })), diff --git a/apps/sim/app/api/chat/manage/[id]/route.ts b/apps/sim/app/api/chat/manage/[id]/route.ts index 8cf37410ae0..dc02fff4d26 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.ts @@ -9,7 +9,7 @@ import { getSession } from '@/lib/auth' import { isDev } from '@/lib/core/config/feature-flags' import { encryptSecret } from '@/lib/core/security/encryption' import { getEmailDomain } from '@/lib/core/utils/urls' -import { performChatUndeploy } from '@/lib/workflows/orchestration' +import { notifySocketDeploymentChanged, performChatUndeploy } from '@/lib/workflows/orchestration' import { deployWorkflow } from '@/lib/workflows/persistence/utils' import { checkChatAccess } from '@/app/api/chat/utils' import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils' @@ -155,6 +155,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< logger.info( `Redeployed workflow ${existingChat[0].workflowId} for chat update (v${deployResult.version})` ) + await notifySocketDeploymentChanged(existingChat[0].workflowId) } let encryptedPassword diff --git a/apps/sim/app/api/form/route.ts b/apps/sim/app/api/form/route.ts index db29c2759de..b42d804203e 100644 --- a/apps/sim/app/api/form/route.ts +++ b/apps/sim/app/api/form/route.ts @@ -10,6 +10,7 @@ import { isDev } from '@/lib/core/config/feature-flags' import { encryptSecret } from '@/lib/core/security/encryption' import { getEmailDomain } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' +import { notifySocketDeploymentChanged } from '@/lib/workflows/orchestration' import { deployWorkflow } from '@/lib/workflows/persistence/utils' import { checkWorkflowAccessForFormCreation, @@ -152,6 +153,8 @@ export async function POST(request: NextRequest) { `${workflowRecord.isDeployed ? 'Redeployed' : 'Auto-deployed'} workflow ${workflowId} for form (v${result.version})` ) + await notifySocketDeploymentChanged(workflowId) + let encryptedPassword = null if (authType === 'password' && password) { const { encrypted } = await encryptSecret(password) diff --git a/apps/sim/app/api/providers/ollama/models/route.ts b/apps/sim/app/api/providers/ollama/models/route.ts index 44434eadca4..4a676f7e7d9 100644 --- a/apps/sim/app/api/providers/ollama/models/route.ts +++ b/apps/sim/app/api/providers/ollama/models/route.ts @@ -1,11 +1,11 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' -import { env } from '@/lib/core/config/env' +import { getOllamaUrl } from '@/lib/core/utils/urls' import type { ModelsObject } from '@/providers/ollama/types' import { filterBlacklistedModels, isProviderBlacklisted } from '@/providers/utils' const logger = createLogger('OllamaModelsAPI') -const OLLAMA_HOST = env.OLLAMA_URL || 'http://localhost:11434' +const OLLAMA_HOST = getOllamaUrl() /** * Get available Ollama models diff --git a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts index 81ef1a01ccf..164344640a3 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts @@ -53,6 +53,7 @@ vi.mock('@/lib/core/utils/request', () => ({ vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: vi.fn().mockReturnValue('http://localhost:3000'), + getOllamaUrl: vi.fn().mockReturnValue('http://localhost:11434'), })) vi.mock('@/lib/execution/call-chain', () => ({ diff --git a/apps/sim/app/api/workflows/[id]/state/route.ts b/apps/sim/app/api/workflows/[id]/state/route.ts index 26a63ecdd81..b9b7e91a79a 100644 --- a/apps/sim/app/api/workflows/[id]/state/route.ts +++ b/apps/sim/app/api/workflows/[id]/state/route.ts @@ -7,6 +7,7 @@ import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { env } from '@/lib/core/config/env' import { generateRequestId } from '@/lib/core/utils/request' +import { getSocketServerUrl } from '@/lib/core/utils/urls' import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence' import { loadWorkflowFromNormalizedTables, @@ -305,8 +306,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ logger.info(`[${requestId}] Successfully saved workflow ${workflowId} state in ${elapsed}ms`) try { - const socketUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002' - const notifyResponse = await fetch(`${socketUrl}/api/workflow-updated`, { + const notifyResponse = await fetch(`${getSocketServerUrl()}/api/workflow-updated`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/apps/sim/app/workspace/providers/socket-provider.tsx b/apps/sim/app/workspace/providers/socket-provider.tsx index 638e0cda063..7af310952bd 100644 --- a/apps/sim/app/workspace/providers/socket-provider.tsx +++ b/apps/sim/app/workspace/providers/socket-provider.tsx @@ -13,7 +13,7 @@ import { import { createLogger } from '@sim/logger' import { useParams } from 'next/navigation' import type { Socket } from 'socket.io-client' -import { getEnv } from '@/lib/core/config/env' +import { getSocketUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' import { type SocketJoinCommand, @@ -102,6 +102,7 @@ interface SocketContextType { onWorkflowDeleted: (handler: (data: any) => void) => void onWorkflowReverted: (handler: (data: any) => void) => void onWorkflowUpdated: (handler: (data: any) => void) => void + onWorkflowDeployed: (handler: (data: any) => void) => void onOperationConfirmed: (handler: (data: any) => void) => void onOperationFailed: (handler: (data: any) => void) => void } @@ -132,6 +133,7 @@ const SocketContext = createContext({ onWorkflowDeleted: () => {}, onWorkflowReverted: () => {}, onWorkflowUpdated: () => {}, + onWorkflowDeployed: () => {}, onOperationConfirmed: () => {}, onOperationFailed: () => {}, }) @@ -176,6 +178,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { workflowDeleted?: (data: any) => void workflowReverted?: (data: any) => void workflowUpdated?: (data: any) => void + workflowDeployed?: (data: any) => void operationConfirmed?: (data: any) => void operationFailed?: (data: any) => void }>({}) @@ -337,7 +340,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { const initializeSocket = async () => { try { const { io } = await import('socket.io-client') - const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || 'http://localhost:3002' + const socketUrl = getSocketUrl() logger.info('Attempting to connect to Socket.IO server', { url: socketUrl, @@ -550,6 +553,11 @@ export function SocketProvider({ children, user }: SocketProviderProps) { eventHandlers.current.workflowUpdated?.(data) }) + socketInstance.on('workflow-deployed', (data) => { + logger.info(`Workflow ${data.workflowId} deployment state changed`) + eventHandlers.current.workflowDeployed?.(data) + }) + const rehydrateWorkflowStores = async (workflowId: string, workflowState: any) => { const [ { useOperationQueueStore }, @@ -994,6 +1002,10 @@ export function SocketProvider({ children, user }: SocketProviderProps) { eventHandlers.current.workflowUpdated = handler }, []) + const onWorkflowDeployed = useCallback((handler: (data: any) => void) => { + eventHandlers.current.workflowDeployed = handler + }, []) + const onOperationConfirmed = useCallback((handler: (data: any) => void) => { eventHandlers.current.operationConfirmed = handler }, []) @@ -1029,6 +1041,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { onWorkflowDeleted, onWorkflowReverted, onWorkflowUpdated, + onWorkflowDeployed, onOperationConfirmed, onOperationFailed, }), @@ -1058,6 +1071,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { onWorkflowDeleted, onWorkflowReverted, onWorkflowUpdated, + onWorkflowDeployed, onOperationConfirmed, onOperationFailed, ] diff --git a/apps/sim/hooks/queries/deployments.ts b/apps/sim/hooks/queries/deployments.ts index a0a559ce802..6e7498f13b6 100644 --- a/apps/sim/hooks/queries/deployments.ts +++ b/apps/sim/hooks/queries/deployments.ts @@ -42,6 +42,8 @@ export function invalidateDeploymentQueries(queryClient: QueryClient, workflowId queryClient.invalidateQueries({ queryKey: deploymentKeys.info(workflowId) }), queryClient.invalidateQueries({ queryKey: deploymentKeys.deployedState(workflowId) }), queryClient.invalidateQueries({ queryKey: deploymentKeys.versions(workflowId) }), + queryClient.invalidateQueries({ queryKey: deploymentKeys.chatStatus(workflowId) }), + queryClient.invalidateQueries({ queryKey: deploymentKeys.formStatus(workflowId) }), ]) } diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index aa1433f7ae6..ae1b2fe90f6 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef } from 'react' import { createLogger } from '@sim/logger' +import { useQueryClient } from '@tanstack/react-query' import type { Edge } from 'reactflow' import { useShallow } from 'zustand/react/shallow' import { useSession } from '@/lib/auth/auth-client' @@ -7,6 +8,7 @@ import { generateId } from '@/lib/core/utils/uuid' import { useSocket } from '@/app/workspace/providers/socket-provider' import { getBlock } from '@/blocks' import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants' +import { invalidateDeploymentQueries } from '@/hooks/queries/deployments' import { useUndoRedo } from '@/hooks/use-undo-redo' import { BLOCK_OPERATIONS, @@ -34,6 +36,7 @@ import { findAllDescendantNodes, isBlockProtected } from '@/stores/workflows/wor const logger = createLogger('CollaborativeWorkflow') export function useCollaborativeWorkflow() { + const queryClient = useQueryClient() const undoRedo = useUndoRedo() const isUndoRedoInProgress = useRef(false) const lastDiffOperationId = useRef(null) @@ -125,6 +128,7 @@ export function useCollaborativeWorkflow() { onWorkflowDeleted, onWorkflowReverted, onWorkflowUpdated, + onWorkflowDeployed, onOperationConfirmed, onOperationFailed, } = useSocket() @@ -645,6 +649,15 @@ export function useCollaborativeWorkflow() { } } + const handleWorkflowDeployed = (data: any) => { + const { workflowId } = data + logger.info(`Workflow ${workflowId} deployment state changed`) + + if (workflowId !== activeWorkflowId) return + + invalidateDeploymentQueries(queryClient, workflowId) + } + const handleOperationConfirmed = (data: any) => { const { operationId } = data logger.debug('Operation confirmed', { operationId }) @@ -664,6 +677,7 @@ export function useCollaborativeWorkflow() { onWorkflowDeleted(handleWorkflowDeleted) onWorkflowReverted(handleWorkflowReverted) onWorkflowUpdated(handleWorkflowUpdated) + onWorkflowDeployed(handleWorkflowDeployed) onOperationConfirmed(handleOperationConfirmed) onOperationFailed(handleOperationFailed) }, [ @@ -673,9 +687,11 @@ export function useCollaborativeWorkflow() { onWorkflowDeleted, onWorkflowReverted, onWorkflowUpdated, + onWorkflowDeployed, onOperationConfirmed, onOperationFailed, activeWorkflowId, + queryClient, confirmOperation, failOperation, emitWorkflowOperation, diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 95538651008..8a64b57a766 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -10,7 +10,7 @@ export interface ToolRuntimeSchemaEntry { } export const TOOL_RUNTIME_SCHEMAS: Record = { - ['agent']: { + agent: { parameters: { properties: { request: { @@ -23,7 +23,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['auth']: { + auth: { parameters: { properties: { request: { @@ -36,7 +36,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['check_deployment_status']: { + check_deployment_status: { parameters: { type: 'object', properties: { @@ -48,7 +48,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['complete_job']: { + complete_job: { parameters: { type: 'object', properties: { @@ -61,7 +61,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['context_write']: { + context_write: { parameters: { type: 'object', properties: { @@ -78,7 +78,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['crawl_website']: { + crawl_website: { parameters: { type: 'object', properties: { @@ -113,7 +113,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_file']: { + create_file: { parameters: { type: 'object', properties: { @@ -149,7 +149,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['create_folder']: { + create_folder: { parameters: { type: 'object', properties: { @@ -170,7 +170,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_job']: { + create_job: { parameters: { type: 'object', properties: { @@ -220,7 +220,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workflow']: { + create_workflow: { parameters: { type: 'object', properties: { @@ -245,7 +245,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workspace_mcp_server']: { + create_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -266,7 +266,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['debug']: { + debug: { parameters: { properties: { context: { @@ -285,7 +285,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_file']: { + delete_file: { parameters: { type: 'object', properties: { @@ -314,7 +314,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['delete_folder']: { + delete_folder: { parameters: { type: 'object', properties: { @@ -330,7 +330,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workflow']: { + delete_workflow: { parameters: { type: 'object', properties: { @@ -346,7 +346,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workspace_mcp_server']: { + delete_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -359,7 +359,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy']: { + deploy: { parameters: { properties: { request: { @@ -373,7 +373,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy_api']: { + deploy_api: { parameters: { type: 'object', properties: { @@ -447,7 +447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_chat']: { + deploy_chat: { parameters: { type: 'object', properties: { @@ -595,7 +595,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_mcp']: { + deploy_mcp: { parameters: { type: 'object', properties: { @@ -711,7 +711,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['deploymentType', 'deploymentStatus'], }, }, - ['download_to_workspace_file']: { + download_to_workspace_file: { parameters: { type: 'object', properties: { @@ -730,7 +730,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['edit_content']: { + edit_content: { parameters: { type: 'object', properties: { @@ -762,7 +762,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['edit_workflow']: { + edit_workflow: { parameters: { type: 'object', properties: { @@ -801,13 +801,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['file']: { + file: { parameters: { type: 'object', }, resultSchema: undefined, }, - ['function_execute']: { + function_execute: { parameters: { type: 'object', properties: { @@ -868,7 +868,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_api_key']: { + generate_api_key: { parameters: { type: 'object', properties: { @@ -886,7 +886,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_image']: { + generate_image: { parameters: { type: 'object', properties: { @@ -923,7 +923,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_visualization']: { + generate_visualization: { parameters: { type: 'object', properties: { @@ -963,7 +963,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_outputs']: { + get_block_outputs: { parameters: { type: 'object', properties: { @@ -984,7 +984,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_upstream_references']: { + get_block_upstream_references: { parameters: { type: 'object', properties: { @@ -1006,7 +1006,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployed_workflow_state']: { + get_deployed_workflow_state: { parameters: { type: 'object', properties: { @@ -1019,7 +1019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployment_version']: { + get_deployment_version: { parameters: { type: 'object', properties: { @@ -1036,7 +1036,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_execution_summary']: { + get_execution_summary: { parameters: { type: 'object', properties: { @@ -1063,7 +1063,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_job_logs']: { + get_job_logs: { parameters: { type: 'object', properties: { @@ -1088,7 +1088,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_page_contents']: { + get_page_contents: { parameters: { type: 'object', properties: { @@ -1116,14 +1116,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_platform_actions']: { + get_platform_actions: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['get_workflow_data']: { + get_workflow_data: { parameters: { type: 'object', properties: { @@ -1142,7 +1142,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_workflow_logs']: { + get_workflow_logs: { parameters: { type: 'object', properties: { @@ -1168,7 +1168,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['glob']: { + glob: { parameters: { type: 'object', properties: { @@ -1187,7 +1187,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['grep']: { + grep: { parameters: { type: 'object', properties: { @@ -1234,7 +1234,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['job']: { + job: { parameters: { properties: { request: { @@ -1247,7 +1247,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge']: { + knowledge: { parameters: { properties: { request: { @@ -1260,7 +1260,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge_base']: { + knowledge_base: { parameters: { type: 'object', properties: { @@ -1452,7 +1452,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['list_folders']: { + list_folders: { parameters: { type: 'object', properties: { @@ -1464,14 +1464,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_user_workspaces']: { + list_user_workspaces: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['list_workspace_mcp_servers']: { + list_workspace_mcp_servers: { parameters: { type: 'object', properties: { @@ -1483,7 +1483,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_credential']: { + manage_credential: { parameters: { type: 'object', properties: { @@ -1512,7 +1512,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_custom_tool']: { + manage_custom_tool: { parameters: { type: 'object', properties: { @@ -1591,7 +1591,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_job']: { + manage_job: { parameters: { type: 'object', properties: { @@ -1661,7 +1661,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_mcp_tool']: { + manage_mcp_tool: { parameters: { type: 'object', properties: { @@ -1712,7 +1712,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_skill']: { + manage_skill: { parameters: { type: 'object', properties: { @@ -1744,7 +1744,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['materialize_file']: { + materialize_file: { parameters: { type: 'object', properties: { @@ -1778,7 +1778,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_folder']: { + move_folder: { parameters: { type: 'object', properties: { @@ -1796,7 +1796,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_workflow']: { + move_workflow: { parameters: { type: 'object', properties: { @@ -1816,7 +1816,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_get_auth_link']: { + oauth_get_auth_link: { parameters: { type: 'object', properties: { @@ -1830,7 +1830,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_request_access']: { + oauth_request_access: { parameters: { type: 'object', properties: { @@ -1844,7 +1844,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['open_resource']: { + open_resource: { parameters: { type: 'object', properties: { @@ -1872,7 +1872,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['read']: { + read: { parameters: { type: 'object', properties: { @@ -1899,7 +1899,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['redeploy']: { + redeploy: { parameters: { type: 'object', properties: { @@ -1967,7 +1967,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['rename_file']: { + rename_file: { parameters: { type: 'object', properties: { @@ -2002,7 +2002,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['rename_workflow']: { + rename_workflow: { parameters: { type: 'object', properties: { @@ -2019,7 +2019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['research']: { + research: { parameters: { properties: { topic: { @@ -2032,7 +2032,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['respond']: { + respond: { parameters: { additionalProperties: true, properties: { @@ -2055,7 +2055,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['restore_resource']: { + restore_resource: { parameters: { type: 'object', properties: { @@ -2073,7 +2073,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['revert_to_version']: { + revert_to_version: { parameters: { type: 'object', properties: { @@ -2090,7 +2090,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run']: { + run: { parameters: { properties: { context: { @@ -2107,7 +2107,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_block']: { + run_block: { parameters: { type: 'object', properties: { @@ -2139,7 +2139,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_from_block']: { + run_from_block: { parameters: { type: 'object', properties: { @@ -2171,7 +2171,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow']: { + run_workflow: { parameters: { type: 'object', properties: { @@ -2199,7 +2199,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow_until_block']: { + run_workflow_until_block: { parameters: { type: 'object', properties: { @@ -2231,7 +2231,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['scrape_page']: { + scrape_page: { parameters: { type: 'object', properties: { @@ -2252,7 +2252,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_documentation']: { + search_documentation: { parameters: { type: 'object', properties: { @@ -2269,7 +2269,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_library_docs']: { + search_library_docs: { parameters: { type: 'object', properties: { @@ -2290,7 +2290,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_online']: { + search_online: { parameters: { type: 'object', properties: { @@ -2331,7 +2331,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_patterns']: { + search_patterns: { parameters: { type: 'object', properties: { @@ -2353,7 +2353,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_block_enabled']: { + set_block_enabled: { parameters: { type: 'object', properties: { @@ -2375,7 +2375,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_environment_variables']: { + set_environment_variables: { parameters: { type: 'object', properties: { @@ -2409,7 +2409,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_global_workflow_variables']: { + set_global_workflow_variables: { parameters: { type: 'object', properties: { @@ -2447,7 +2447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['superagent']: { + superagent: { parameters: { properties: { task: { @@ -2461,7 +2461,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['table']: { + table: { parameters: { properties: { request: { @@ -2474,7 +2474,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['tool_search_tool_regex']: { + tool_search_tool_regex: { parameters: { properties: { case_insensitive: { @@ -2495,7 +2495,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_job_history']: { + update_job_history: { parameters: { type: 'object', properties: { @@ -2513,7 +2513,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_workspace_mcp_server']: { + update_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -2538,7 +2538,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_memory']: { + user_memory: { parameters: { type: 'object', properties: { @@ -2586,7 +2586,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_table']: { + user_table: { parameters: { type: 'object', properties: { @@ -2777,13 +2777,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['workflow']: { + workflow: { parameters: { type: 'object', }, resultSchema: undefined, }, - ['workspace_file']: { + workspace_file: { parameters: { type: 'object', properties: { diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts index 4168e681bf5..bb408783036 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts @@ -6,6 +6,7 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { env } from '@/lib/core/config/env' import { generateRequestId } from '@/lib/core/utils/request' +import { getSocketServerUrl } from '@/lib/core/utils/urls' import { generateId } from '@/lib/core/utils/uuid' import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow' import { @@ -147,8 +148,7 @@ function findDescendants(containerId: string, blocksById: Record logger.info('Workflow state persisted to database', { workflowId }) - const socketUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002' - fetch(`${socketUrl}/api/workflow-updated`, { + fetch(`${getSocketServerUrl()}/api/workflow-updated`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/apps/sim/lib/core/security/csp.ts b/apps/sim/lib/core/security/csp.ts index 9420878e6d5..073a0c35f97 100644 --- a/apps/sim/lib/core/security/csp.ts +++ b/apps/sim/lib/core/security/csp.ts @@ -3,8 +3,19 @@ import { isDev, isHosted, isReactGrabEnabled } from '../config/feature-flags' /** * Content Security Policy (CSP) configuration builder + * + * NOTE: This file is loaded by next.config.ts at build time, before @/ path + * aliases are resolved. Do NOT import from ../utils/urls (which uses @/ imports). + * Keep all URL constants local to this file. */ +const DEFAULT_SOCKET_URL = 'http://localhost:3002' +const DEFAULT_OLLAMA_URL = 'http://localhost:11434' + +function toWebSocketUrl(httpUrl: string): string { + return httpUrl.replace('http://', 'ws://').replace('https://', 'wss://') +} + function getHostnameFromUrl(url: string | undefined): string[] { if (!url) return [] try { @@ -156,14 +167,11 @@ export const buildTimeCSPDirectives: CSPDirectives = { 'connect-src': [ ...STATIC_CONNECT_SRC, env.NEXT_PUBLIC_APP_URL || '', - ...(env.OLLAMA_URL ? [env.OLLAMA_URL] : isDev ? ['http://localhost:11434'] : []), + ...(env.OLLAMA_URL ? [env.OLLAMA_URL] : isDev ? [DEFAULT_OLLAMA_URL] : []), ...(env.NEXT_PUBLIC_SOCKET_URL - ? [ - env.NEXT_PUBLIC_SOCKET_URL, - env.NEXT_PUBLIC_SOCKET_URL.replace('http://', 'ws://').replace('https://', 'wss://'), - ] + ? [env.NEXT_PUBLIC_SOCKET_URL, toWebSocketUrl(env.NEXT_PUBLIC_SOCKET_URL)] : isDev - ? ['http://localhost:3002', 'ws://localhost:3002'] + ? [DEFAULT_SOCKET_URL, toWebSocketUrl(DEFAULT_SOCKET_URL)] : []), ...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL), ...getHostnameFromUrl(env.NEXT_PUBLIC_PRIVACY_URL), @@ -201,13 +209,9 @@ export function buildCSPString(directives: CSPDirectives): string { export function generateRuntimeCSP(): string { const appUrl = getEnv('NEXT_PUBLIC_APP_URL') || '' - const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || (isDev ? 'http://localhost:3002' : '') - const socketWsUrl = socketUrl - ? socketUrl.replace('http://', 'ws://').replace('https://', 'wss://') - : isDev - ? 'ws://localhost:3002' - : '' - const ollamaUrl = getEnv('OLLAMA_URL') || (isDev ? 'http://localhost:11434' : '') + const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || (isDev ? DEFAULT_SOCKET_URL : '') + const socketWsUrl = socketUrl ? toWebSocketUrl(socketUrl) : '' + const ollamaUrl = getEnv('OLLAMA_URL') || (isDev ? DEFAULT_OLLAMA_URL : '') const brandLogoDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_BRAND_LOGO_URL')) const brandFaviconDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL')) diff --git a/apps/sim/lib/core/utils/urls.ts b/apps/sim/lib/core/utils/urls.ts index 15712176a8d..8381fe58ca0 100644 --- a/apps/sim/lib/core/utils/urls.ts +++ b/apps/sim/lib/core/utils/urls.ts @@ -1,4 +1,4 @@ -import { getEnv } from '@/lib/core/config/env' +import { env, getEnv } from '@/lib/core/config/env' import { isProd } from '@/lib/core/config/feature-flags' /** Canonical base URL for the public-facing marketing site. No trailing slash. */ @@ -100,3 +100,30 @@ export function getEmailDomain(): string { return isProd ? 'sim.ai' : 'localhost:3000' } } + +const DEFAULT_SOCKET_URL = 'http://localhost:3002' +const DEFAULT_OLLAMA_URL = 'http://localhost:11434' + +/** + * Returns the socket server URL for server-side internal API calls. + * Reads from SOCKET_SERVER_URL with a localhost fallback for development. + */ +export function getSocketServerUrl(): string { + return env.SOCKET_SERVER_URL || DEFAULT_SOCKET_URL +} + +/** + * Returns the socket server URL for client-side Socket.IO connections. + * Reads from NEXT_PUBLIC_SOCKET_URL with a localhost fallback for development. + */ +export function getSocketUrl(): string { + return getEnv('NEXT_PUBLIC_SOCKET_URL') || DEFAULT_SOCKET_URL +} + +/** + * Returns the Ollama server URL. + * Reads from OLLAMA_URL with a localhost fallback for development. + */ +export function getOllamaUrl(): string { + return env.OLLAMA_URL || DEFAULT_OLLAMA_URL +} diff --git a/apps/sim/lib/workflows/lifecycle.test.ts b/apps/sim/lib/workflows/lifecycle.test.ts index 473ff68a3c3..28c3b2386ee 100644 --- a/apps/sim/lib/workflows/lifecycle.test.ts +++ b/apps/sim/lib/workflows/lifecycle.test.ts @@ -56,6 +56,11 @@ vi.mock('@/lib/core/config/env', () => ({ SOCKET_SERVER_URL: 'http://socket.test', INTERNAL_API_SECRET: 'secret', }, + getEnv: vi.fn(), +})) + +vi.mock('@/lib/core/utils/urls', () => ({ + getSocketServerUrl: vi.fn().mockReturnValue('http://socket.test'), })) vi.mock('@/lib/core/telemetry', () => ({ diff --git a/apps/sim/lib/workflows/lifecycle.ts b/apps/sim/lib/workflows/lifecycle.ts index c9ff0a9bcfa..d42dca12ca2 100644 --- a/apps/sim/lib/workflows/lifecycle.ts +++ b/apps/sim/lib/workflows/lifecycle.ts @@ -18,6 +18,7 @@ import { env } from '@/lib/core/config/env' import { getRedisClient } from '@/lib/core/config/redis' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' +import { getSocketServerUrl } from '@/lib/core/utils/urls' import { mcpPubSub } from '@/lib/mcp/pubsub' import { getWorkflowById } from '@/lib/workflows/utils' @@ -31,8 +32,7 @@ interface ArchiveWorkflowOptions { async function notifyWorkflowArchived(workflowId: string, requestId: string): Promise { try { - const socketUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002' - const socketResponse = await fetch(`${socketUrl}/api/workflow-deleted`, { + const socketResponse = await fetch(`${getSocketServerUrl()}/api/workflow-deleted`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/apps/sim/lib/workflows/orchestration/deploy.ts b/apps/sim/lib/workflows/orchestration/deploy.ts index 56717aa877d..4f81701b558 100644 --- a/apps/sim/lib/workflows/orchestration/deploy.ts +++ b/apps/sim/lib/workflows/orchestration/deploy.ts @@ -5,7 +5,7 @@ import { NextRequest } from 'next/server' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { env } from '@/lib/core/config/env' import { generateRequestId } from '@/lib/core/utils/request' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { getBaseUrl, getSocketServerUrl } from '@/lib/core/utils/urls' import { removeMcpToolsForWorkflow, syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync' import { captureServerEvent } from '@/lib/posthog/server' import { @@ -31,6 +31,30 @@ import type { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('DeployOrchestration') +/** + * Notifies the socket server that a workflow's deployment state has changed, + * so all connected clients can refresh their deployment queries. + */ +export async function notifySocketDeploymentChanged(workflowId: string): Promise { + try { + const response = await fetch(`${getSocketServerUrl()}/api/workflow-deployed`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': env.INTERNAL_API_SECRET, + }, + body: JSON.stringify({ workflowId }), + }) + if (!response.ok) { + logger.warn( + `Socket deployment notification failed (${response.status}) for workflow ${workflowId}` + ) + } + } catch (error) { + logger.error('Error sending workflow deployed event to socket server', error) + } +} + export interface PerformFullDeployParams { workflowId: string userId: string @@ -222,6 +246,8 @@ export async function performFullDeploy( request, }) + await notifySocketDeploymentChanged(workflowId) + return { success: true, deployedAt, @@ -296,6 +322,8 @@ export async function performFullUndeploy( description: `Undeployed workflow "${(workflowData.name as string) || workflowId}"`, }) + await notifySocketDeploymentChanged(workflowId) + return { success: true } } @@ -509,6 +537,8 @@ export async function performActivateVersion( }, }) + await notifySocketDeploymentChanged(workflowId) + return { success: true, deployedAt: result.deployedAt, @@ -596,8 +626,7 @@ export async function performRevertToVersion( .where(eq(workflowTable.id, workflowId)) try { - const socketServerUrl = env.SOCKET_SERVER_URL || 'http://localhost:3002' - await fetch(`${socketServerUrl}/api/workflow-reverted`, { + await fetch(`${getSocketServerUrl()}/api/workflow-reverted`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/apps/sim/lib/workflows/orchestration/index.ts b/apps/sim/lib/workflows/orchestration/index.ts index a4aeeec42e2..a055f6a3532 100644 --- a/apps/sim/lib/workflows/orchestration/index.ts +++ b/apps/sim/lib/workflows/orchestration/index.ts @@ -7,6 +7,7 @@ export { performChatUndeploy, } from './chat-deploy' export { + notifySocketDeploymentChanged, type PerformActivateVersionParams, type PerformActivateVersionResult, type PerformFullDeployParams, diff --git a/apps/sim/providers/ollama/index.ts b/apps/sim/providers/ollama/index.ts index b6cb8dd3234..c09bf88977b 100644 --- a/apps/sim/providers/ollama/index.ts +++ b/apps/sim/providers/ollama/index.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import OpenAI from 'openai' import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions' -import { env } from '@/lib/core/config/env' +import { getOllamaUrl } from '@/lib/core/utils/urls' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import type { ModelsObject } from '@/providers/ollama/types' @@ -18,7 +18,7 @@ import { useProvidersStore } from '@/stores/providers' import { executeTool } from '@/tools' const logger = createLogger('OllamaProvider') -const OLLAMA_HOST = env.OLLAMA_URL || 'http://localhost:11434' +const OLLAMA_HOST = getOllamaUrl() export const ollamaProvider: ProviderConfig = { id: 'ollama', diff --git a/apps/sim/socket/rooms/memory-manager.ts b/apps/sim/socket/rooms/memory-manager.ts index fa631ff6898..1e14c9c7df1 100644 --- a/apps/sim/socket/rooms/memory-manager.ts +++ b/apps/sim/socket/rooms/memory-manager.ts @@ -238,4 +238,21 @@ export class MemoryRoomManager implements IRoomManager { logger.info(`Notified ${room.users.size} users about workflow update: ${workflowId}`) } + + async handleWorkflowDeployed(workflowId: string): Promise { + logger.info(`Handling workflow deployed notification for ${workflowId}`) + + const room = this.workflowRooms.get(workflowId) + if (!room) { + logger.debug(`No active room found for deployed workflow ${workflowId}`) + return + } + + this._io.to(workflowId).emit('workflow-deployed', { + workflowId, + timestamp: Date.now(), + }) + + logger.info(`Notified ${room.users.size} users about workflow deployment change: ${workflowId}`) + } } diff --git a/apps/sim/socket/rooms/redis-manager.ts b/apps/sim/socket/rooms/redis-manager.ts index fb0d0d1042b..adfe6720485 100644 --- a/apps/sim/socket/rooms/redis-manager.ts +++ b/apps/sim/socket/rooms/redis-manager.ts @@ -439,4 +439,22 @@ export class RedisRoomManager implements IRoomManager { const userCount = await this.getUniqueUserCount(workflowId) logger.info(`Notified ${userCount} users about workflow update: ${workflowId}`) } + + async handleWorkflowDeployed(workflowId: string): Promise { + logger.info(`Handling workflow deployed notification for ${workflowId}`) + + const hasRoom = await this.hasWorkflowRoom(workflowId) + if (!hasRoom) { + logger.debug(`No active room found for deployed workflow ${workflowId}`) + return + } + + this._io.to(workflowId).emit('workflow-deployed', { + workflowId, + timestamp: Date.now(), + }) + + const userCount = await this.getUniqueUserCount(workflowId) + logger.info(`Notified ${userCount} users about workflow deployment change: ${workflowId}`) + } } diff --git a/apps/sim/socket/rooms/types.ts b/apps/sim/socket/rooms/types.ts index 5c755a739e0..9553a427e1e 100644 --- a/apps/sim/socket/rooms/types.ts +++ b/apps/sim/socket/rooms/types.ts @@ -138,4 +138,9 @@ export interface IRoomManager { * Handle workflow update - notify users */ handleWorkflowUpdate(workflowId: string): Promise + + /** + * Handle workflow deployment change - notify users to refresh deployment state + */ + handleWorkflowDeployed(workflowId: string): Promise } diff --git a/apps/sim/socket/routes/http.ts b/apps/sim/socket/routes/http.ts index ea2eb3cde76..5c555e92843 100644 --- a/apps/sim/socket/routes/http.ts +++ b/apps/sim/socket/routes/http.ts @@ -122,6 +122,20 @@ export function createHttpHandler(roomManager: IRoomManager, logger: Logger) { return } + // Handle workflow deployment change notifications from the main API + if (req.method === 'POST' && req.url === '/api/workflow-deployed') { + try { + const body = await readRequestBody(req) + const { workflowId } = JSON.parse(body) + await roomManager.handleWorkflowDeployed(workflowId) + sendSuccess(res) + } catch (error) { + logger.error('Error handling workflow deployed notification:', error) + sendError(res, 'Failed to process deployment notification') + } + return + } + // Handle workflow revert notifications from the main API if (req.method === 'POST' && req.url === '/api/workflow-reverted') { try {