d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
281 lines
11 KiB
TypeScript
281 lines
11 KiB
TypeScript
import { createLogger } from '@sim/logger'
|
|
import { z } from 'zod'
|
|
import { getBlockVisibilityForCopilot } from '@/lib/copilot/block-visibility'
|
|
import {
|
|
CreateFile,
|
|
CreateFileFolder,
|
|
DeleteFile,
|
|
DeleteFileFolder,
|
|
DownloadToWorkspaceFile,
|
|
Ffmpeg,
|
|
GenerateAudio,
|
|
GenerateImage,
|
|
GenerateVideo,
|
|
KnowledgeBase,
|
|
ManageCredential,
|
|
ManageCustomTool,
|
|
ManageMcpTool,
|
|
ManageSkill,
|
|
MoveFile,
|
|
MoveFileFolder,
|
|
RenameFile,
|
|
RenameFileFolder,
|
|
UserTable,
|
|
WorkspaceFile,
|
|
} from '@/lib/copilot/generated/tool-catalog-v1'
|
|
import {
|
|
assertServerToolNotAborted,
|
|
type BaseServerTool,
|
|
type ServerToolContext,
|
|
} from '@/lib/copilot/tools/server/base-tool'
|
|
import { getBlocksMetadataServerTool } from '@/lib/copilot/tools/server/blocks/get-blocks-metadata-tool'
|
|
import { getTriggerBlocksServerTool } from '@/lib/copilot/tools/server/blocks/get-trigger-blocks'
|
|
import { searchDocumentationServerTool } from '@/lib/copilot/tools/server/docs/search-documentation'
|
|
import { enrichmentRunServerTool } from '@/lib/copilot/tools/server/enrichment/enrichment-run'
|
|
import { createFileServerTool } from '@/lib/copilot/tools/server/files/create-file'
|
|
import { deleteFileServerTool } from '@/lib/copilot/tools/server/files/delete-file'
|
|
import { downloadToWorkspaceFileServerTool } from '@/lib/copilot/tools/server/files/download-to-workspace-file'
|
|
import { editContentServerTool } from '@/lib/copilot/tools/server/files/edit-content'
|
|
import {
|
|
createFileFolderServerTool,
|
|
deleteFileFolderServerTool,
|
|
listFileFoldersServerTool,
|
|
moveFileFolderServerTool,
|
|
moveFileServerTool,
|
|
renameFileFolderServerTool,
|
|
} from '@/lib/copilot/tools/server/files/file-folders'
|
|
import { renameFileServerTool } from '@/lib/copilot/tools/server/files/rename-file'
|
|
import { workspaceFileServerTool } from '@/lib/copilot/tools/server/files/workspace-file'
|
|
import { validateGeneratedToolPayload } from '@/lib/copilot/tools/server/generated-schema'
|
|
import { generateImageServerTool } from '@/lib/copilot/tools/server/image/generate-image'
|
|
import { getJobLogsServerTool } from '@/lib/copilot/tools/server/jobs/get-job-logs'
|
|
import { knowledgeBaseServerTool } from '@/lib/copilot/tools/server/knowledge/knowledge-base'
|
|
import { ffmpegServerTool } from '@/lib/copilot/tools/server/media/ffmpeg'
|
|
import { generateAudioServerTool } from '@/lib/copilot/tools/server/media/generate-audio'
|
|
import { generateVideoServerTool } from '@/lib/copilot/tools/server/media/generate-video'
|
|
import { searchOnlineServerTool } from '@/lib/copilot/tools/server/other/search-online'
|
|
import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table'
|
|
import { getCredentialsServerTool } from '@/lib/copilot/tools/server/user/get-credentials'
|
|
import { setEnvironmentVariablesServerTool } from '@/lib/copilot/tools/server/user/set-environment-variables'
|
|
import { editWorkflowServerTool } from '@/lib/copilot/tools/server/workflow/edit-workflow'
|
|
import { queryLogsServerTool } from '@/lib/copilot/tools/server/workflow/query-logs'
|
|
import { listCustomBlocksWithInputsForWorkspace } from '@/lib/workflows/custom-blocks/operations'
|
|
import { withCustomBlockOverlay } from '@/blocks/custom/server-overlay'
|
|
import { withBlockVisibility } from '@/blocks/visibility/server-context'
|
|
|
|
export type ExecuteResponseSuccess = z.output<typeof ExecuteResponseSuccessSchema>
|
|
|
|
const ExecuteResponseSuccessSchema = z.object({
|
|
success: z.literal(true),
|
|
result: z.unknown(),
|
|
})
|
|
|
|
const logger = createLogger('ServerToolRouter')
|
|
|
|
/**
|
|
* Tools that resolve blocks through the registry (`getBlock`/`getAllBlocks`) and
|
|
* must run inside the custom-block overlay so `custom_block_*` types resolve.
|
|
*/
|
|
const CUSTOM_BLOCK_OVERLAY_TOOLS = new Set(['edit_workflow', 'get_blocks_metadata'])
|
|
|
|
/**
|
|
* DISCOVERY tools that must run inside the viewer's block-visibility context so
|
|
* gated (preview / kill-switched) blocks disappear from what the agent can
|
|
* list. Deliberately a DIFFERENT set from {@link CUSTOM_BLOCK_OVERLAY_TOOLS}:
|
|
* `edit_workflow` is excluded because its registry use is functional
|
|
* (find-by-type over clones, never a discovery listing) and gating it would
|
|
* only risk leaking display projections into persisted state.
|
|
*/
|
|
const VISIBILITY_GATED_TOOLS = new Set(['get_blocks_metadata', 'get_trigger_blocks'])
|
|
|
|
const WRITE_ACTIONS: Record<string, string[]> = {
|
|
[KnowledgeBase.id]: [
|
|
'create',
|
|
'add_file',
|
|
'update',
|
|
'delete',
|
|
'delete_document',
|
|
'update_document',
|
|
'create_tag',
|
|
'update_tag',
|
|
'delete_tag',
|
|
'add_connector',
|
|
'update_connector',
|
|
'delete_connector',
|
|
'sync_connector',
|
|
],
|
|
[UserTable.id]: [
|
|
'create',
|
|
'create_from_file',
|
|
'import_file',
|
|
'delete',
|
|
'insert_row',
|
|
'batch_insert_rows',
|
|
'update_row',
|
|
'batch_update_rows',
|
|
'delete_row',
|
|
'batch_delete_rows',
|
|
'update_rows_by_filter',
|
|
'delete_rows_by_filter',
|
|
'add_column',
|
|
'rename_column',
|
|
'delete_column',
|
|
'update_column',
|
|
'add_enrichment',
|
|
],
|
|
[ManageCustomTool.id]: ['add', 'edit', 'delete'],
|
|
[ManageMcpTool.id]: ['add', 'edit', 'delete'],
|
|
[ManageSkill.id]: ['add', 'edit', 'delete'],
|
|
[ManageCredential.id]: ['rename', 'delete'],
|
|
[WorkspaceFile.id]: ['create', 'append', 'update', 'delete', 'rename', 'patch'],
|
|
[editContentServerTool.name]: ['*'],
|
|
[CreateFile.id]: ['*'],
|
|
[RenameFile.id]: ['*'],
|
|
[DeleteFile.id]: ['*'],
|
|
[MoveFile.id]: ['*'],
|
|
[CreateFileFolder.id]: ['*'],
|
|
[RenameFileFolder.id]: ['*'],
|
|
[MoveFileFolder.id]: ['*'],
|
|
[DeleteFileFolder.id]: ['*'],
|
|
[DownloadToWorkspaceFile.id]: ['*'],
|
|
[GenerateImage.id]: ['generate'],
|
|
[GenerateVideo.id]: ['generate'],
|
|
[GenerateAudio.id]: ['generate'],
|
|
[Ffmpeg.id]: ['*'],
|
|
// Paid external-provider lookups (hosted-key cost), like the media tools.
|
|
[enrichmentRunServerTool.name]: ['*'],
|
|
}
|
|
|
|
function isWritePermission(userPermission: string): boolean {
|
|
return userPermission === 'write' || userPermission === 'admin'
|
|
}
|
|
|
|
function isWriteAction(toolName: string, action: string | undefined): boolean {
|
|
const writeActions = WRITE_ACTIONS[toolName]
|
|
if (!writeActions) return false
|
|
// '*' means the tool is always a write operation regardless of action field
|
|
if (writeActions.includes('*')) return true
|
|
return Boolean(action && writeActions.includes(action))
|
|
}
|
|
|
|
/** Registry of all server tools. Tools self-declare their validation schemas. */
|
|
const baseServerToolRegistry: Record<string, BaseServerTool> = {
|
|
[getBlocksMetadataServerTool.name]: getBlocksMetadataServerTool,
|
|
[getTriggerBlocksServerTool.name]: getTriggerBlocksServerTool,
|
|
[editWorkflowServerTool.name]: editWorkflowServerTool,
|
|
[queryLogsServerTool.name]: queryLogsServerTool,
|
|
[getJobLogsServerTool.name]: getJobLogsServerTool,
|
|
[searchDocumentationServerTool.name]: searchDocumentationServerTool,
|
|
[searchOnlineServerTool.name]: searchOnlineServerTool,
|
|
[setEnvironmentVariablesServerTool.name]: setEnvironmentVariablesServerTool,
|
|
[getCredentialsServerTool.name]: getCredentialsServerTool,
|
|
[knowledgeBaseServerTool.name]: knowledgeBaseServerTool,
|
|
[enrichmentRunServerTool.name]: enrichmentRunServerTool,
|
|
[userTableServerTool.name]: userTableServerTool,
|
|
[workspaceFileServerTool.name]: workspaceFileServerTool,
|
|
[editContentServerTool.name]: editContentServerTool,
|
|
[createFileServerTool.name]: createFileServerTool,
|
|
[renameFileServerTool.name]: renameFileServerTool,
|
|
[deleteFileServerTool.name]: deleteFileServerTool,
|
|
[moveFileServerTool.name]: moveFileServerTool,
|
|
[listFileFoldersServerTool.name]: listFileFoldersServerTool,
|
|
[createFileFolderServerTool.name]: createFileFolderServerTool,
|
|
[renameFileFolderServerTool.name]: renameFileFolderServerTool,
|
|
[moveFileFolderServerTool.name]: moveFileFolderServerTool,
|
|
[deleteFileFolderServerTool.name]: deleteFileFolderServerTool,
|
|
[downloadToWorkspaceFileServerTool.name]: downloadToWorkspaceFileServerTool,
|
|
[generateImageServerTool.name]: generateImageServerTool,
|
|
[generateVideoServerTool.name]: generateVideoServerTool,
|
|
[generateAudioServerTool.name]: generateAudioServerTool,
|
|
[ffmpegServerTool.name]: ffmpegServerTool,
|
|
}
|
|
|
|
function getServerToolRegistry(): Record<string, BaseServerTool> {
|
|
return baseServerToolRegistry
|
|
}
|
|
|
|
export function getRegisteredServerToolNames(): string[] {
|
|
return Object.keys(getServerToolRegistry())
|
|
}
|
|
|
|
export async function routeExecution(
|
|
toolName: string,
|
|
payload: unknown,
|
|
context?: ServerToolContext
|
|
): Promise<unknown> {
|
|
const tool = getServerToolRegistry()[toolName]
|
|
if (!tool) {
|
|
throw new Error(`Unknown server tool: ${toolName}`)
|
|
}
|
|
|
|
logger.debug(
|
|
context?.messageId ? `Routing to tool [messageId:${context.messageId}]` : 'Routing to tool',
|
|
{ toolName }
|
|
)
|
|
|
|
// Action-level permission enforcement for mixed read/write tools
|
|
if (WRITE_ACTIONS[toolName]) {
|
|
const p = payload as Record<string, unknown>
|
|
const action = (p?.operation ?? p?.action) as string | undefined
|
|
if (isWriteAction(toolName, action) && !isWritePermission(context?.userPermission ?? '')) {
|
|
const actionLabel = action ? `'${action}' on ` : ''
|
|
throw new Error(
|
|
`Permission denied: ${actionLabel}${toolName} requires write access. You have '${context?.userPermission ?? 'none'}' permission.`
|
|
)
|
|
}
|
|
}
|
|
|
|
assertServerToolNotAborted(
|
|
context,
|
|
`User stop signal aborted ${toolName} before payload normalization`
|
|
)
|
|
|
|
// Go injects chatId/workspaceId and may wrap the model's args inside a
|
|
// nested "args" object. Unwrap that before validation so the generated
|
|
// JSON Schema sees the flat tool contract shape.
|
|
let normalizedPayload = payload ?? {}
|
|
if (
|
|
normalizedPayload &&
|
|
typeof normalizedPayload === 'object' &&
|
|
!Array.isArray(normalizedPayload)
|
|
) {
|
|
const raw = normalizedPayload as Record<string, unknown>
|
|
if (raw.args && typeof raw.args === 'object' && !raw.operation) {
|
|
const nested = raw.args as Record<string, unknown>
|
|
normalizedPayload = { ...nested, ...raw, args: undefined }
|
|
}
|
|
}
|
|
|
|
const args = tool.inputSchema
|
|
? tool.inputSchema.parse(normalizedPayload)
|
|
: validateGeneratedToolPayload(toolName, 'parameters', normalizedPayload)
|
|
|
|
assertServerToolNotAborted(context, `User stop signal aborted ${toolName} after validation`)
|
|
|
|
// Execute. The registry-dependent tools resolve blocks via getBlock/getAllBlocks;
|
|
// wrap them in the custom-block overlay for the workspace's org so `custom_block_*`
|
|
// types resolve (metadata lookup + edit-workflow validation) instead of being
|
|
// rejected as unknown, and wrap discovery tools in the viewer's block-visibility
|
|
// context so gated blocks stay hidden. The two ALS scopes are independent and
|
|
// nest in either order. Other tools skip the extra queries.
|
|
let run = () => tool.execute(args, context)
|
|
if (VISIBILITY_GATED_TOOLS.has(toolName) && context?.userId) {
|
|
// Memoized per (userId, workspaceId) ~30s — a multi-tool turn resolves once.
|
|
const vis = await getBlockVisibilityForCopilot(context.userId, context.workspaceId)
|
|
const inner = run
|
|
run = () => withBlockVisibility(vis, inner)
|
|
}
|
|
if (CUSTOM_BLOCK_OVERLAY_TOOLS.has(toolName) && context?.workspaceId) {
|
|
const rows = await listCustomBlocksWithInputsForWorkspace(context.workspaceId)
|
|
const inner = run
|
|
run = () => withCustomBlockOverlay(rows, inner)
|
|
}
|
|
const result = await run()
|
|
|
|
// Validate output if tool declares a schema; otherwise fall back to the
|
|
// generated JSON schema contract emitted from Go.
|
|
return tool.outputSchema
|
|
? tool.outputSchema.parse(result)
|
|
: validateGeneratedToolPayload(toolName, 'resultSchema', result)
|
|
}
|