Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

287 lines
7.8 KiB
TypeScript

import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types'
import { captureServerEvent } from '@/lib/posthog/server'
import {
deleteCustomTool,
getCustomToolById,
listCustomTools,
upsertCustomTools,
} from '@/lib/workflows/custom-tools/operations'
const logger = createLogger('CopilotToolExecutor')
type ManageCustomToolOperation = 'add' | 'edit' | 'delete' | 'list'
interface ManageCustomToolSchema {
type: 'function'
function: {
name: string
description?: string
parameters: Record<string, unknown>
}
}
interface ManageCustomToolParams {
operation?: string
toolId?: string
toolIds?: string[]
schema?: ManageCustomToolSchema
code?: string
title?: string
workspaceId?: string
}
export async function executeManageCustomTool(
rawParams: Record<string, unknown>,
context: ExecutionContext
): Promise<ToolCallResult> {
const params = rawParams as ManageCustomToolParams
const operation = String(params.operation || '').toLowerCase() as ManageCustomToolOperation
const workspaceId = params.workspaceId || context.workspaceId
if (!operation) {
return { success: false, error: "Missing required 'operation' argument" }
}
const writeOps: string[] = ['add', 'edit', 'delete']
if (
writeOps.includes(operation) &&
context.userPermission &&
context.userPermission !== 'write' &&
context.userPermission !== 'admin'
) {
return {
success: false,
error: `Permission denied: '${operation}' on manage_custom_tool requires write access. You have '${context.userPermission}' permission.`,
}
}
try {
if (operation === 'list') {
const toolsForUser = await listCustomTools({
userId: context.userId,
workspaceId,
})
return {
success: true,
output: {
success: true,
operation,
tools: toolsForUser,
count: toolsForUser.length,
},
}
}
if (operation === 'add') {
if (!workspaceId) {
return {
success: false,
error: "workspaceId is required for operation 'add'",
}
}
if (!params.schema || !params.code) {
return {
success: false,
error: "Both 'schema' and 'code' are required for operation 'add'",
}
}
const title = params.title || params.schema.function?.name
if (!title) {
return { success: false, error: "Missing tool title or schema.function.name for 'add'" }
}
const resultTools = await upsertCustomTools({
tools: [{ title, schema: params.schema, code: params.code }],
workspaceId,
userId: context.userId,
})
const created = resultTools.find((tool) => tool.title === title)
recordAudit({
workspaceId,
actorId: context.userId,
action: AuditAction.CUSTOM_TOOL_CREATED,
resourceType: AuditResourceType.CUSTOM_TOOL,
resourceId: created?.id,
resourceName: title,
description: `Created custom tool "${title}"`,
metadata: { source: 'tool_input' },
})
if (created?.id) {
captureServerEvent(
context.userId,
'custom_tool_saved',
{
tool_id: created.id,
workspace_id: workspaceId,
tool_name: title,
source: 'tool_input',
},
{ groups: { workspace: workspaceId } }
)
}
return {
success: true,
output: {
success: true,
operation,
toolId: created?.id,
title,
message: `Created custom tool "${title}"`,
},
}
}
if (operation === 'edit') {
if (!workspaceId) {
return {
success: false,
error: "workspaceId is required for operation 'edit'",
}
}
if (!params.toolId) {
return { success: false, error: "'toolId' is required for operation 'edit'" }
}
if (!params.schema && !params.code) {
return {
success: false,
error: "At least one of 'schema' or 'code' is required for operation 'edit'",
}
}
const existing = await getCustomToolById({
toolId: params.toolId,
userId: context.userId,
workspaceId,
})
if (!existing) {
return { success: false, error: `Custom tool not found: ${params.toolId}` }
}
const mergedSchema = params.schema || (existing.schema as ManageCustomToolSchema)
const mergedCode = params.code || existing.code
const title = params.title || mergedSchema.function?.name || existing.title
await upsertCustomTools({
tools: [{ id: params.toolId, title, schema: mergedSchema, code: mergedCode }],
workspaceId,
userId: context.userId,
})
recordAudit({
workspaceId,
actorId: context.userId,
action: AuditAction.CUSTOM_TOOL_UPDATED,
resourceType: AuditResourceType.CUSTOM_TOOL,
resourceId: params.toolId,
resourceName: title,
description: `Updated custom tool "${title}"`,
metadata: { source: 'tool_input' },
})
captureServerEvent(
context.userId,
'custom_tool_saved',
{
tool_id: params.toolId,
workspace_id: workspaceId,
tool_name: title,
source: 'tool_input',
},
{ groups: { workspace: workspaceId } }
)
return {
success: true,
output: {
success: true,
operation,
toolId: params.toolId,
title,
message: `Updated custom tool "${title}"`,
},
}
}
if (operation === 'delete') {
const toolIds: string[] = params.toolIds ?? (params.toolId ? [params.toolId] : [])
if (toolIds.length === 0) {
return { success: false, error: "'toolId' or 'toolIds' is required for operation 'delete'" }
}
const deleted: string[] = []
const notFound: string[] = []
for (const toolId of toolIds) {
const result = await deleteCustomTool({
toolId,
userId: context.userId,
workspaceId,
})
if (result) {
deleted.push(toolId)
} else {
notFound.push(toolId)
}
}
for (const toolId of deleted) {
recordAudit({
workspaceId: workspaceId ?? null,
actorId: context.userId,
action: AuditAction.CUSTOM_TOOL_DELETED,
resourceType: AuditResourceType.CUSTOM_TOOL,
resourceId: toolId,
description: 'Deleted custom tool',
metadata: { source: 'tool_input' },
})
if (workspaceId) {
captureServerEvent(
context.userId,
'custom_tool_deleted',
{ tool_id: toolId, workspace_id: workspaceId, source: 'tool_input' },
{ groups: { workspace: workspaceId } }
)
}
}
return {
success: deleted.length > 0,
output: {
success: deleted.length > 0,
operation,
deleted,
notFound,
message: `Deleted ${deleted.length} custom tool(s)`,
},
}
}
return {
success: false,
error: `Unsupported operation for manage_custom_tool: ${operation}`,
}
} catch (error) {
logger.error(
context.messageId
? `manage_custom_tool execution failed [messageId:${context.messageId}]`
: 'manage_custom_tool execution failed',
{
operation,
workspaceId,
userId: context.userId,
error: toError(error).message,
}
)
return {
success: false,
error: getErrorMessage(error, 'Failed to manage custom tool'),
}
}
}