mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Checkpoint
This commit is contained in:
@@ -10,6 +10,9 @@ export const INTERRUPT_TOOL_NAMES = [
|
||||
'deploy_chat',
|
||||
'deploy_api',
|
||||
'create_workspace_mcp_server',
|
||||
'update_workspace_mcp_server',
|
||||
'delete_workspace_mcp_server',
|
||||
'delete_workflow',
|
||||
'set_environment_variables',
|
||||
'make_api_request',
|
||||
'oauth_request_access',
|
||||
|
||||
@@ -217,13 +217,6 @@ export async function executeDeployMcp(
|
||||
return { success: false, error: 'workspaceId is required' }
|
||||
}
|
||||
|
||||
if (!workflowRecord.isDeployed) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Workflow must be deployed before adding as an MCP tool. Use deploy_api first.',
|
||||
}
|
||||
}
|
||||
|
||||
const serverId = params.serverId
|
||||
if (!serverId) {
|
||||
return {
|
||||
@@ -232,6 +225,34 @@ export async function executeDeployMcp(
|
||||
}
|
||||
}
|
||||
|
||||
// Handle undeploy action — remove workflow from MCP server
|
||||
if (params.action === 'undeploy') {
|
||||
const deleted = await db
|
||||
.delete(workflowMcpTool)
|
||||
.where(
|
||||
and(eq(workflowMcpTool.serverId, serverId), eq(workflowMcpTool.workflowId, workflowId))
|
||||
)
|
||||
.returning({ id: workflowMcpTool.id })
|
||||
|
||||
if (deleted.length === 0) {
|
||||
return { success: false, error: 'Workflow is not deployed to this MCP server' }
|
||||
}
|
||||
|
||||
mcpPubSub?.publishWorkflowToolsChanged({ serverId, workspaceId })
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { workflowId, serverId, action: 'undeploy', removed: true },
|
||||
}
|
||||
}
|
||||
|
||||
if (!workflowRecord.isDeployed) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Workflow must be deployed before adding as an MCP tool. Use deploy_api first.',
|
||||
}
|
||||
}
|
||||
|
||||
const existingTool = await db
|
||||
.select()
|
||||
.from(workflowMcpTool)
|
||||
|
||||
@@ -11,7 +11,9 @@ import { ensureWorkflowAccess } from '../access'
|
||||
import type {
|
||||
CheckDeploymentStatusParams,
|
||||
CreateWorkspaceMcpServerParams,
|
||||
DeleteWorkspaceMcpServerParams,
|
||||
ListWorkspaceMcpServersParams,
|
||||
UpdateWorkspaceMcpServerParams,
|
||||
} from '../param-types'
|
||||
|
||||
export async function executeCheckDeploymentStatus(
|
||||
@@ -231,3 +233,82 @@ export async function executeCreateWorkspaceMcpServer(
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeUpdateWorkspaceMcpServer(
|
||||
params: UpdateWorkspaceMcpServerParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const serverId = params.serverId
|
||||
if (!serverId) {
|
||||
return { success: false, error: 'serverId is required' }
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = { updatedAt: new Date() }
|
||||
|
||||
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 <= 1) {
|
||||
return { success: false, error: 'At least one of name, description, or isPublic is required' }
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select({ id: workflowMcpServer.id, createdBy: workflowMcpServer.createdBy })
|
||||
.from(workflowMcpServer)
|
||||
.where(eq(workflowMcpServer.id, serverId))
|
||||
.limit(1)
|
||||
|
||||
if (!existing) {
|
||||
return { success: false, error: 'MCP server not found' }
|
||||
}
|
||||
|
||||
await db
|
||||
.update(workflowMcpServer)
|
||||
.set(updates)
|
||||
.where(eq(workflowMcpServer.id, serverId))
|
||||
|
||||
return { success: true, output: { serverId, ...updates, updatedAt: undefined } }
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeDeleteWorkspaceMcpServer(
|
||||
params: DeleteWorkspaceMcpServerParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
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(eq(workflowMcpServer.id, serverId))
|
||||
.limit(1)
|
||||
|
||||
if (!existing) {
|
||||
return { success: false, error: 'MCP server not found' }
|
||||
}
|
||||
|
||||
await db.delete(workflowMcpServer).where(eq(workflowMcpServer.id, serverId))
|
||||
|
||||
mcpPubSub?.publishWorkflowToolsChanged({ serverId, workspaceId: existing.workspaceId })
|
||||
|
||||
return { success: true, output: { serverId, name: existing.name, deleted: true } }
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ import { getTool, resolveToolId } from '@/tools/utils'
|
||||
import {
|
||||
executeCheckDeploymentStatus,
|
||||
executeCreateWorkspaceMcpServer,
|
||||
executeDeleteWorkspaceMcpServer,
|
||||
executeDeployApi,
|
||||
executeDeployChat,
|
||||
executeDeployMcp,
|
||||
executeListWorkspaceMcpServers,
|
||||
executeRedeploy,
|
||||
executeUpdateWorkspaceMcpServer,
|
||||
} from './deployment-tools'
|
||||
import { executeIntegrationToolDirect } from './integration-tools'
|
||||
import {
|
||||
@@ -35,6 +37,9 @@ import type {
|
||||
CreateFolderParams,
|
||||
CreateWorkflowParams,
|
||||
CreateWorkspaceMcpServerParams,
|
||||
DeleteFolderParams,
|
||||
DeleteWorkflowParams,
|
||||
DeleteWorkspaceMcpServerParams,
|
||||
DeployApiParams,
|
||||
DeployChatParams,
|
||||
DeployMcpParams,
|
||||
@@ -47,17 +52,22 @@ import type {
|
||||
ListWorkspaceMcpServersParams,
|
||||
MoveFolderParams,
|
||||
MoveWorkflowParams,
|
||||
RenameFolderParams,
|
||||
RenameWorkflowParams,
|
||||
RunBlockParams,
|
||||
RunFromBlockParams,
|
||||
RunWorkflowParams,
|
||||
RunWorkflowUntilBlockParams,
|
||||
SetGlobalWorkflowVariablesParams,
|
||||
UpdateWorkflowParams,
|
||||
UpdateWorkspaceMcpServerParams,
|
||||
} from './param-types'
|
||||
import { PLATFORM_ACTIONS_CONTENT } from './platform-actions'
|
||||
import {
|
||||
executeCreateFolder,
|
||||
executeCreateWorkflow,
|
||||
executeDeleteFolder,
|
||||
executeDeleteWorkflow,
|
||||
executeGenerateApiKey,
|
||||
executeGetBlockOutputs,
|
||||
executeGetBlockUpstreamReferences,
|
||||
@@ -67,12 +77,14 @@ import {
|
||||
executeListUserWorkspaces,
|
||||
executeMoveFolder,
|
||||
executeMoveWorkflow,
|
||||
executeRenameFolder,
|
||||
executeRenameWorkflow,
|
||||
executeRunBlock,
|
||||
executeRunFromBlock,
|
||||
executeRunWorkflow,
|
||||
executeRunWorkflowUntilBlock,
|
||||
executeSetGlobalWorkflowVariables,
|
||||
executeUpdateWorkflow,
|
||||
} from './workflow-tools'
|
||||
|
||||
const logger = createLogger('CopilotToolExecutor')
|
||||
@@ -339,8 +351,12 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
|
||||
create_workflow: (p, c) => executeCreateWorkflow(p as CreateWorkflowParams, c),
|
||||
create_folder: (p, c) => executeCreateFolder(p as CreateFolderParams, c),
|
||||
rename_workflow: (p, c) => executeRenameWorkflow(p as unknown as RenameWorkflowParams, c),
|
||||
update_workflow: (p, c) => executeUpdateWorkflow(p as unknown as UpdateWorkflowParams, c),
|
||||
delete_workflow: (p, c) => executeDeleteWorkflow(p as unknown as DeleteWorkflowParams, c),
|
||||
move_workflow: (p, c) => executeMoveWorkflow(p as unknown as MoveWorkflowParams, c),
|
||||
move_folder: (p, c) => executeMoveFolder(p as unknown as MoveFolderParams, c),
|
||||
rename_folder: (p, c) => executeRenameFolder(p as unknown as RenameFolderParams, c),
|
||||
delete_folder: (p, c) => executeDeleteFolder(p as unknown as DeleteFolderParams, c),
|
||||
get_workflow_data: (p, c) => executeGetWorkflowData(p as GetWorkflowDataParams, c),
|
||||
get_block_outputs: (p, c) => executeGetBlockOutputs(p as GetBlockOutputsParams, c),
|
||||
get_block_upstream_references: (p, c) =>
|
||||
@@ -370,6 +386,10 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
|
||||
executeListWorkspaceMcpServers(p as ListWorkspaceMcpServersParams, c),
|
||||
create_workspace_mcp_server: (p, c) =>
|
||||
executeCreateWorkspaceMcpServer(p as CreateWorkspaceMcpServerParams, c),
|
||||
update_workspace_mcp_server: (p, c) =>
|
||||
executeUpdateWorkspaceMcpServer(p as unknown as UpdateWorkspaceMcpServerParams, c),
|
||||
delete_workspace_mcp_server: (p, c) =>
|
||||
executeDeleteWorkspaceMcpServer(p as unknown as DeleteWorkspaceMcpServerParams, c),
|
||||
oauth_get_auth_link: async (p, _c) => {
|
||||
const providerName = (p.providerName || p.provider_name || 'the provider') as string
|
||||
try {
|
||||
|
||||
@@ -163,6 +163,16 @@ export interface RenameWorkflowParams {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface UpdateWorkflowParams {
|
||||
workflowId: string
|
||||
name?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface DeleteWorkflowParams {
|
||||
workflowId: string
|
||||
}
|
||||
|
||||
export interface MoveWorkflowParams {
|
||||
workflowId: string
|
||||
folderId: string | null
|
||||
@@ -172,3 +182,23 @@ export interface MoveFolderParams {
|
||||
folderId: string
|
||||
parentId: string | null
|
||||
}
|
||||
|
||||
export interface RenameFolderParams {
|
||||
folderId: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface DeleteFolderParams {
|
||||
folderId: string
|
||||
}
|
||||
|
||||
export interface UpdateWorkspaceMcpServerParams {
|
||||
serverId: string
|
||||
name?: string
|
||||
description?: string
|
||||
isPublic?: boolean
|
||||
}
|
||||
|
||||
export interface DeleteWorkspaceMcpServerParams {
|
||||
serverId: string
|
||||
}
|
||||
|
||||
@@ -18,15 +18,19 @@ import { ensureWorkflowAccess, ensureWorkspaceAccess, getDefaultWorkspaceId } fr
|
||||
import type {
|
||||
CreateFolderParams,
|
||||
CreateWorkflowParams,
|
||||
DeleteFolderParams,
|
||||
DeleteWorkflowParams,
|
||||
GenerateApiKeyParams,
|
||||
MoveFolderParams,
|
||||
MoveWorkflowParams,
|
||||
RenameFolderParams,
|
||||
RenameWorkflowParams,
|
||||
RunBlockParams,
|
||||
RunFromBlockParams,
|
||||
RunWorkflowParams,
|
||||
RunWorkflowUntilBlockParams,
|
||||
SetGlobalWorkflowVariablesParams,
|
||||
UpdateWorkflowParams,
|
||||
VariableOperation,
|
||||
} from '../param-types'
|
||||
|
||||
@@ -566,6 +570,142 @@ export async function executeRunFromBlock(
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeUpdateWorkflow(
|
||||
params: UpdateWorkflowParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = { updatedAt: new Date() }
|
||||
|
||||
if (typeof params.name === 'string') {
|
||||
const name = params.name.trim()
|
||||
if (!name) return { success: false, error: 'name cannot be empty' }
|
||||
if (name.length > 200) return { success: false, error: 'Workflow name must be 200 characters or less' }
|
||||
updates.name = name
|
||||
}
|
||||
|
||||
if (typeof params.description === 'string') {
|
||||
if (params.description.length > 2000) {
|
||||
return { success: false, error: 'Description must be 2000 characters or less' }
|
||||
}
|
||||
updates.description = params.description
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length <= 1) {
|
||||
return { success: false, error: 'At least one of name or description is required' }
|
||||
}
|
||||
|
||||
await ensureWorkflowAccess(workflowId, context.userId)
|
||||
|
||||
await db.update(workflow).set(updates).where(eq(workflow.id, workflowId))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { workflowId, ...updates, updatedAt: undefined },
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeDeleteWorkflow(
|
||||
params: DeleteWorkflowParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workflowId = params.workflowId
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
|
||||
const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId)
|
||||
|
||||
await db.delete(workflow).where(eq(workflow.id, workflowId))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { workflowId, name: workflowRecord.name, deleted: true },
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeRenameFolder(
|
||||
params: RenameFolderParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const folderId = params.folderId
|
||||
if (!folderId) {
|
||||
return { success: false, error: 'folderId is required' }
|
||||
}
|
||||
const name = typeof params.name === 'string' ? params.name.trim() : ''
|
||||
if (!name) {
|
||||
return { success: false, error: 'name is required' }
|
||||
}
|
||||
if (name.length > 200) {
|
||||
return { success: false, error: 'Folder name must be 200 characters or less' }
|
||||
}
|
||||
|
||||
await db
|
||||
.update(workflowFolder)
|
||||
.set({ name, updatedAt: new Date() })
|
||||
.where(eq(workflowFolder.id, folderId))
|
||||
|
||||
return { success: true, output: { folderId, name } }
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeDeleteFolder(
|
||||
params: DeleteFolderParams,
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const folderId = params.folderId
|
||||
if (!folderId) {
|
||||
return { success: false, error: 'folderId is required' }
|
||||
}
|
||||
|
||||
// Get the folder to find its parent
|
||||
const [folder] = await db
|
||||
.select({ parentId: workflowFolder.parentId })
|
||||
.from(workflowFolder)
|
||||
.where(eq(workflowFolder.id, folderId))
|
||||
.limit(1)
|
||||
|
||||
if (!folder) {
|
||||
return { success: false, error: 'Folder not found' }
|
||||
}
|
||||
|
||||
// Move child workflows to parent folder
|
||||
await db
|
||||
.update(workflow)
|
||||
.set({ folderId: folder.parentId, updatedAt: new Date() })
|
||||
.where(eq(workflow.folderId, folderId))
|
||||
|
||||
// Move child folders to parent folder
|
||||
await db
|
||||
.update(workflowFolder)
|
||||
.set({ parentId: folder.parentId, updatedAt: new Date() })
|
||||
.where(eq(workflowFolder.parentId, folderId))
|
||||
|
||||
// Delete the folder
|
||||
await db.delete(workflowFolder).where(eq(workflowFolder.id, folderId))
|
||||
|
||||
return { success: true, output: { folderId, deleted: true } }
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeRunBlock(
|
||||
params: RunBlockParams,
|
||||
context: ExecutionContext
|
||||
|
||||
@@ -4,8 +4,10 @@ import type { KnowledgeBaseArgs, KnowledgeBaseResult } from '@/lib/copilot/tools
|
||||
import { generateSearchEmbedding } from '@/lib/knowledge/embeddings'
|
||||
import {
|
||||
createKnowledgeBase,
|
||||
deleteKnowledgeBase,
|
||||
getKnowledgeBaseById,
|
||||
getKnowledgeBases,
|
||||
updateKnowledgeBase,
|
||||
} from '@/lib/knowledge/service'
|
||||
import {
|
||||
createTagDefinition,
|
||||
@@ -221,6 +223,83 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
|
||||
}
|
||||
}
|
||||
|
||||
case 'update': {
|
||||
if (!args.knowledgeBaseId) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Knowledge base ID is required for update operation',
|
||||
}
|
||||
}
|
||||
|
||||
const updates: { name?: string; description?: string; chunkingConfig?: { maxSize: number; minSize: number; overlap: number } } = {}
|
||||
if (args.name) updates.name = args.name
|
||||
if (args.description !== undefined) updates.description = args.description
|
||||
if (args.chunkingConfig) updates.chunkingConfig = args.chunkingConfig
|
||||
|
||||
if (!updates.name && updates.description === undefined && !updates.chunkingConfig) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'At least one of name, description, or chunkingConfig is required for update',
|
||||
}
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const updatedKb = await updateKnowledgeBase(args.knowledgeBaseId, updates, requestId)
|
||||
|
||||
logger.info('Knowledge base updated via copilot', {
|
||||
knowledgeBaseId: args.knowledgeBaseId,
|
||||
userId: context.userId,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Knowledge base "${updatedKb.name}" updated successfully`,
|
||||
data: {
|
||||
id: updatedKb.id,
|
||||
name: updatedKb.name,
|
||||
description: updatedKb.description,
|
||||
workspaceId: updatedKb.workspaceId,
|
||||
docCount: updatedKb.docCount,
|
||||
updatedAt: updatedKb.updatedAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
if (!args.knowledgeBaseId) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Knowledge base ID is required for delete operation',
|
||||
}
|
||||
}
|
||||
|
||||
const kbToDelete = await getKnowledgeBaseById(args.knowledgeBaseId)
|
||||
if (!kbToDelete) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Knowledge base with ID "${args.knowledgeBaseId}" not found`,
|
||||
}
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
await deleteKnowledgeBase(args.knowledgeBaseId, requestId)
|
||||
|
||||
logger.info('Knowledge base deleted via copilot', {
|
||||
knowledgeBaseId: args.knowledgeBaseId,
|
||||
name: kbToDelete.name,
|
||||
userId: context.userId,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Knowledge base "${kbToDelete.name}" deleted successfully`,
|
||||
data: {
|
||||
id: args.knowledgeBaseId,
|
||||
name: kbToDelete.name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
case 'list_tags': {
|
||||
if (!args.knowledgeBaseId) {
|
||||
return {
|
||||
@@ -391,7 +470,7 @@ export const knowledgeBaseServerTool: BaseServerTool<KnowledgeBaseArgs, Knowledg
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
message: `Unknown operation: ${operation}. Supported operations: create, list, get, query, list_tags, create_tag, update_tag, delete_tag, get_tag_usage`,
|
||||
message: `Unknown operation: ${operation}. Supported operations: create, list, get, query, update, delete, list_tags, create_tag, update_tag, delete_tag, get_tag_usage`,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -26,6 +26,8 @@ export const KnowledgeBaseArgsSchema = z.object({
|
||||
'list',
|
||||
'get',
|
||||
'query',
|
||||
'update',
|
||||
'delete',
|
||||
'list_tags',
|
||||
'create_tag',
|
||||
'update_tag',
|
||||
|
||||
@@ -243,6 +243,140 @@ export function serializeEnvironmentVariables(
|
||||
)
|
||||
}
|
||||
|
||||
/** Input types for deployment serialization. */
|
||||
export interface DeploymentData {
|
||||
workflowId: string
|
||||
isDeployed: boolean
|
||||
deployedAt?: Date | null
|
||||
api?: {
|
||||
version: number
|
||||
createdAt: Date
|
||||
} | null
|
||||
chat?: {
|
||||
id: string
|
||||
identifier: string
|
||||
title: string
|
||||
description?: string | null
|
||||
authType: string
|
||||
customizations: unknown
|
||||
isActive: boolean
|
||||
} | null
|
||||
form?: {
|
||||
id: string
|
||||
identifier: string
|
||||
title: string
|
||||
description?: string | null
|
||||
authType: string
|
||||
showBranding: boolean
|
||||
customizations: unknown
|
||||
isActive: boolean
|
||||
} | null
|
||||
mcp: Array<{
|
||||
serverId: string
|
||||
serverName: string
|
||||
toolId: string
|
||||
toolName: string
|
||||
toolDescription?: string | null
|
||||
}>
|
||||
a2a?: {
|
||||
id: string
|
||||
name: string
|
||||
description?: string | null
|
||||
version: string
|
||||
isPublished: boolean
|
||||
capabilities: unknown
|
||||
} | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize all deployment configurations for VFS deployment.json.
|
||||
* Only includes keys for active deployment types.
|
||||
*/
|
||||
export function serializeDeployments(data: DeploymentData): string {
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
if (data.isDeployed) {
|
||||
result.api = {
|
||||
isDeployed: true,
|
||||
deployedAt: data.deployedAt?.toISOString(),
|
||||
apiEndpoint: `/api/workflows/${data.workflowId}/run`,
|
||||
...(data.api ? { version: data.api.version } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
if (data.chat) {
|
||||
result.chat = {
|
||||
id: data.chat.id,
|
||||
identifier: data.chat.identifier,
|
||||
chatUrl: `/chat/${data.chat.identifier}`,
|
||||
title: data.chat.title,
|
||||
description: data.chat.description || undefined,
|
||||
authType: data.chat.authType,
|
||||
customizations: data.chat.customizations,
|
||||
isActive: data.chat.isActive,
|
||||
}
|
||||
}
|
||||
|
||||
if (data.form) {
|
||||
result.form = {
|
||||
id: data.form.id,
|
||||
identifier: data.form.identifier,
|
||||
formUrl: `/form/${data.form.identifier}`,
|
||||
title: data.form.title,
|
||||
description: data.form.description || undefined,
|
||||
authType: data.form.authType,
|
||||
showBranding: data.form.showBranding,
|
||||
customizations: data.form.customizations,
|
||||
isActive: data.form.isActive,
|
||||
}
|
||||
}
|
||||
|
||||
if (data.mcp.length > 0) {
|
||||
result.mcp = data.mcp.map((m) => ({
|
||||
serverId: m.serverId,
|
||||
serverName: m.serverName,
|
||||
toolId: m.toolId,
|
||||
toolName: m.toolName,
|
||||
toolDescription: m.toolDescription || undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
if (data.a2a) {
|
||||
result.a2a = {
|
||||
id: data.a2a.id,
|
||||
name: data.a2a.name,
|
||||
description: data.a2a.description || undefined,
|
||||
version: data.a2a.version,
|
||||
isPublished: data.a2a.isPublished,
|
||||
capabilities: data.a2a.capabilities,
|
||||
agentUrl: `/api/a2a/serve/${data.a2a.id}`,
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(result, null, 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a custom tool for VFS custom-tools/{name}.json
|
||||
*/
|
||||
export function serializeCustomTool(tool: {
|
||||
id: string
|
||||
title: string
|
||||
schema: unknown
|
||||
code: string
|
||||
}): string {
|
||||
return JSON.stringify(
|
||||
{
|
||||
id: tool.id,
|
||||
title: tool.title,
|
||||
schema: tool.schema,
|
||||
codePreview: tool.code.length > 500 ? tool.code.slice(0, 500) + '...' : tool.code,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an integration/tool schema for VFS components/integrations/{service}/{operation}.json
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { db } from '@sim/db'
|
||||
import {
|
||||
a2aAgent,
|
||||
account,
|
||||
apiKey,
|
||||
chat as chatTable,
|
||||
customTools,
|
||||
document,
|
||||
environment,
|
||||
form,
|
||||
knowledgeBase,
|
||||
workflow,
|
||||
workflowDeploymentVersion,
|
||||
workflowMcpServer,
|
||||
workflowMcpTool,
|
||||
workspaceEnvironment,
|
||||
workflowExecutionLogs,
|
||||
} from '@sim/db/schema'
|
||||
@@ -22,6 +29,8 @@ import {
|
||||
serializeApiKeys,
|
||||
serializeBlockSchema,
|
||||
serializeCredentials,
|
||||
serializeCustomTool,
|
||||
serializeDeployments,
|
||||
serializeDocuments,
|
||||
serializeEnvironmentVariables,
|
||||
serializeIntegrationSchema,
|
||||
@@ -29,6 +38,7 @@ import {
|
||||
serializeRecentExecutions,
|
||||
serializeWorkflowMeta,
|
||||
} from '@/lib/copilot/vfs/serializers'
|
||||
import type { DeploymentData } from '@/lib/copilot/vfs/serializers'
|
||||
|
||||
const logger = createLogger('WorkspaceVFS')
|
||||
|
||||
@@ -114,8 +124,10 @@ function getStaticComponentFiles(): Map<string, string> {
|
||||
* workflows/{name}/blocks.json
|
||||
* workflows/{name}/edges.json
|
||||
* workflows/{name}/executions.json
|
||||
* workflows/{name}/deployment.json
|
||||
* knowledgebases/{name}/meta.json
|
||||
* knowledgebases/{name}/documents.json
|
||||
* custom-tools/{name}.json
|
||||
* environment/credentials.json
|
||||
* environment/api-keys.json
|
||||
* environment/variables.json
|
||||
@@ -137,6 +149,7 @@ export class WorkspaceVFS {
|
||||
this.materializeWorkflows(workspaceId, userId),
|
||||
this.materializeKnowledgeBases(workspaceId),
|
||||
this.materializeEnvironment(workspaceId, userId),
|
||||
this.materializeCustomTools(workspaceId),
|
||||
])
|
||||
|
||||
// Merge static component files
|
||||
@@ -253,6 +266,19 @@ export class WorkspaceVFS {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
|
||||
// Deployment configuration
|
||||
try {
|
||||
const deploymentData = await this.getWorkflowDeployments(wf.id, workspaceId, wf.isDeployed, wf.deployedAt)
|
||||
if (deploymentData) {
|
||||
this.files.set(`${prefix}deployment.json`, serializeDeployments(deploymentData))
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Failed to load deployment data', {
|
||||
workflowId: wf.id,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -317,6 +343,134 @@ export class WorkspaceVFS {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query all deployment configurations for a single workflow.
|
||||
* Returns null if the workflow has no deployments of any kind.
|
||||
*/
|
||||
private async getWorkflowDeployments(
|
||||
workflowId: string,
|
||||
workspaceId: string,
|
||||
isDeployed: boolean,
|
||||
deployedAt: Date | null
|
||||
): Promise<DeploymentData | null> {
|
||||
const [chatRows, formRows, mcpRows, a2aRows, versionRows] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: chatTable.id,
|
||||
identifier: chatTable.identifier,
|
||||
title: chatTable.title,
|
||||
description: chatTable.description,
|
||||
authType: chatTable.authType,
|
||||
customizations: chatTable.customizations,
|
||||
isActive: chatTable.isActive,
|
||||
})
|
||||
.from(chatTable)
|
||||
.where(eq(chatTable.workflowId, workflowId)),
|
||||
db
|
||||
.select({
|
||||
id: form.id,
|
||||
identifier: form.identifier,
|
||||
title: form.title,
|
||||
description: form.description,
|
||||
authType: form.authType,
|
||||
showBranding: form.showBranding,
|
||||
customizations: form.customizations,
|
||||
isActive: form.isActive,
|
||||
})
|
||||
.from(form)
|
||||
.where(eq(form.workflowId, workflowId)),
|
||||
db
|
||||
.select({
|
||||
serverId: workflowMcpTool.serverId,
|
||||
serverName: workflowMcpServer.name,
|
||||
toolId: workflowMcpTool.id,
|
||||
toolName: workflowMcpTool.toolName,
|
||||
toolDescription: workflowMcpTool.toolDescription,
|
||||
})
|
||||
.from(workflowMcpTool)
|
||||
.innerJoin(workflowMcpServer, eq(workflowMcpTool.serverId, workflowMcpServer.id))
|
||||
.where(eq(workflowMcpTool.workflowId, workflowId)),
|
||||
db
|
||||
.select({
|
||||
id: a2aAgent.id,
|
||||
name: a2aAgent.name,
|
||||
description: a2aAgent.description,
|
||||
version: a2aAgent.version,
|
||||
isPublished: a2aAgent.isPublished,
|
||||
capabilities: a2aAgent.capabilities,
|
||||
})
|
||||
.from(a2aAgent)
|
||||
.where(
|
||||
and(eq(a2aAgent.workflowId, workflowId), eq(a2aAgent.workspaceId, workspaceId))
|
||||
),
|
||||
isDeployed
|
||||
? db
|
||||
.select({
|
||||
version: workflowDeploymentVersion.version,
|
||||
createdAt: workflowDeploymentVersion.createdAt,
|
||||
})
|
||||
.from(workflowDeploymentVersion)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowDeploymentVersion.workflowId, workflowId),
|
||||
eq(workflowDeploymentVersion.isActive, true)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
: Promise.resolve([]),
|
||||
])
|
||||
|
||||
const hasAnyDeployment =
|
||||
isDeployed || chatRows.length > 0 || formRows.length > 0 || mcpRows.length > 0 || a2aRows.length > 0
|
||||
if (!hasAnyDeployment) return null
|
||||
|
||||
return {
|
||||
workflowId,
|
||||
isDeployed,
|
||||
deployedAt,
|
||||
api: versionRows[0] ?? null,
|
||||
chat: chatRows[0] ?? null,
|
||||
form: formRows[0] ?? null,
|
||||
mcp: mcpRows,
|
||||
a2a: a2aRows[0] ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize all custom tools in the workspace.
|
||||
*/
|
||||
private async materializeCustomTools(workspaceId: string): Promise<void> {
|
||||
try {
|
||||
const toolRows = await db
|
||||
.select({
|
||||
id: customTools.id,
|
||||
title: customTools.title,
|
||||
schema: customTools.schema,
|
||||
code: customTools.code,
|
||||
})
|
||||
.from(customTools)
|
||||
.where(eq(customTools.workspaceId, workspaceId))
|
||||
|
||||
for (const tool of toolRows) {
|
||||
const safeName = sanitizeName(tool.title)
|
||||
this.files.set(
|
||||
`custom-tools/${safeName}.json`,
|
||||
serializeCustomTool({
|
||||
id: tool.id,
|
||||
title: tool.title,
|
||||
schema: tool.schema,
|
||||
code: tool.code,
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('Failed to materialize custom tools', {
|
||||
workspaceId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize environment data: credentials, API keys, env variable names.
|
||||
*/
|
||||
@@ -422,3 +576,18 @@ function sanitizeName(name: string): string {
|
||||
.replace(/^-|-$/g, '')
|
||||
.slice(0, 64)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduplicate a sanitized name against a set of already-used names.
|
||||
* Appends `-2`, `-3`, etc. on collision. Adds the final name to the set.
|
||||
*/
|
||||
function deduplicateName(baseName: string, usedNames: Set<string>): string {
|
||||
let name = baseName
|
||||
let counter = 2
|
||||
while (usedNames.has(name)) {
|
||||
name = `${baseName}-${counter}`
|
||||
counter++
|
||||
}
|
||||
usedNames.add(name)
|
||||
return name
|
||||
}
|
||||
|
||||
@@ -47,6 +47,41 @@ You have access to these specialized subagents. Call them by name to delegate ta
|
||||
- **memory_file_read(file_path)** — Read a persistent memory file.
|
||||
- **memory_file_write(file_path, content)** — Write/update a persistent memory file.
|
||||
- **memory_file_list()** — List all memory files.
|
||||
- **grep(pattern, path?)** — Search workspace VFS file contents.
|
||||
- **glob(pattern)** — Find workspace VFS files by path pattern.
|
||||
- **read(path)** — Read a workspace VFS file.
|
||||
- **list(path)** — List workspace VFS directory entries.
|
||||
- **create_workflow(name, description?)** — Create a new workflow.
|
||||
- **update_workflow(workflowId, name?, description?)** — Update workflow name or description.
|
||||
- **delete_workflow(workflowId)** — Delete a workflow.
|
||||
- **rename_folder(folderId, name)** — Rename a folder.
|
||||
- **delete_folder(folderId)** — Delete a folder (moves contents to parent).
|
||||
|
||||
## Workspace Virtual Filesystem (VFS)
|
||||
|
||||
Your workspace data is available as a virtual filesystem. Use grep/glob/read/list to explore it before taking action.
|
||||
|
||||
\`\`\`
|
||||
workflows/{name}/
|
||||
meta.json — name, description, id, run stats
|
||||
blocks.json — workflow block graph (sanitized)
|
||||
edges.json — block connections
|
||||
executions.json — last 5 run results
|
||||
deployment.json — all deployment configs (api, chat, form, mcp, a2a)
|
||||
knowledgebases/{name}/
|
||||
meta.json — KB identity, embedding config, stats
|
||||
documents.json — document metadata
|
||||
custom-tools/{name}.json — custom tool schema + code preview
|
||||
environment/
|
||||
credentials.json — connected OAuth providers
|
||||
api-keys.json — API key metadata (names, not values)
|
||||
variables.json — env variable names (not values)
|
||||
components/
|
||||
blocks/{type}.json — block type schemas
|
||||
integrations/{svc}/{op}.json — integration tool schemas
|
||||
\`\`\`
|
||||
|
||||
**Tips**: Use \`glob("workflows/*/deployment.json")\` to see which workflows are deployed and how. Use \`grep("error", "workflows/")\` to find workflows with recent errors.
|
||||
|
||||
## Memory Management
|
||||
|
||||
|
||||
Reference in New Issue
Block a user