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
287 lines
7.8 KiB
TypeScript
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'),
|
|
}
|
|
}
|
|
}
|