mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-10 14:45:16 -05:00
Fix superagent and autoallow integrations
This commit is contained in:
@@ -1239,8 +1239,8 @@ function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
const mode = useCopilotStore.getState().mode
|
||||
if (mode === 'build' && isIntegrationTool(toolCall.name)) {
|
||||
// Integration tools (user-installed) always require approval
|
||||
if (isIntegrationTool(toolCall.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ export async function buildCopilotRequestPayload(
|
||||
messages.push({ role: 'user', content: message })
|
||||
}
|
||||
|
||||
let integrationTools: ToolSchema[] = []
|
||||
const integrationTools: ToolSchema[] = []
|
||||
let credentials: CredentialsPayload | null = null
|
||||
|
||||
if (effectiveMode === 'build') {
|
||||
@@ -195,22 +195,29 @@ export async function buildCopilotRequestPayload(
|
||||
const { createUserToolSchema } = await import('@/tools/params')
|
||||
const latestTools = getLatestVersionTools(tools)
|
||||
|
||||
integrationTools = Object.entries(latestTools).map(([toolId, toolConfig]) => {
|
||||
const userSchema = createUserToolSchema(toolConfig)
|
||||
const strippedName = stripVersionSuffix(toolId)
|
||||
return {
|
||||
name: strippedName,
|
||||
description: toolConfig.description || toolConfig.name || strippedName,
|
||||
input_schema: userSchema as unknown as Record<string, unknown>,
|
||||
defer_loading: true,
|
||||
...(toolConfig.oauth?.required && {
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: toolConfig.oauth.provider,
|
||||
},
|
||||
}),
|
||||
for (const [toolId, toolConfig] of Object.entries(latestTools)) {
|
||||
try {
|
||||
const userSchema = createUserToolSchema(toolConfig)
|
||||
const strippedName = stripVersionSuffix(toolId)
|
||||
integrationTools.push({
|
||||
name: strippedName,
|
||||
description: toolConfig.description || toolConfig.name || strippedName,
|
||||
input_schema: userSchema as unknown as Record<string, unknown>,
|
||||
defer_loading: true,
|
||||
...(toolConfig.oauth?.required && {
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: toolConfig.oauth.provider,
|
||||
},
|
||||
}),
|
||||
})
|
||||
} catch (toolError) {
|
||||
logger.warn('Failed to build schema for tool, skipping', {
|
||||
toolId,
|
||||
error: toolError instanceof Error ? toolError.message : String(toolError),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Failed to build tool schemas for payload', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { STREAM_STORAGE_KEY } from '@/lib/copilot/constants'
|
||||
import { COPILOT_CONFIRM_API_PATH, STREAM_STORAGE_KEY } from '@/lib/copilot/constants'
|
||||
import { asRecord } from '@/lib/copilot/orchestrator/sse-utils'
|
||||
import type { SSEEvent } from '@/lib/copilot/orchestrator/types'
|
||||
import {
|
||||
@@ -24,6 +24,23 @@ const MAX_BATCH_INTERVAL = 50
|
||||
const MIN_BATCH_INTERVAL = 16
|
||||
const MAX_QUEUE_SIZE = 5
|
||||
|
||||
/**
|
||||
* Send an auto-accept confirmation to the server for auto-allowed tools.
|
||||
* The server-side orchestrator polls Redis for this decision.
|
||||
*/
|
||||
export function sendAutoAcceptConfirmation(toolCallId: string): void {
|
||||
fetch(COPILOT_CONFIRM_API_PATH, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ toolCallId, status: 'accepted' }),
|
||||
}).catch((error) => {
|
||||
logger.warn('Failed to send auto-accept confirmation', {
|
||||
toolCallId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function writeActiveStreamToStorage(info: CopilotStreamInfo | null): void {
|
||||
if (typeof window === 'undefined') return
|
||||
try {
|
||||
@@ -565,6 +582,12 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-allowed tools: send confirmation to the server so it can proceed
|
||||
// without waiting for the user to click "Allow".
|
||||
if (isAutoAllowed) {
|
||||
sendAutoAcceptConfirmation(id)
|
||||
}
|
||||
|
||||
// OAuth: dispatch event to open the OAuth connect modal
|
||||
if (toolName === 'oauth_request_access' && args && typeof window !== 'undefined') {
|
||||
try {
|
||||
|
||||
@@ -9,7 +9,12 @@ import type { SSEEvent } from '@/lib/copilot/orchestrator/types'
|
||||
import { resolveToolDisplay } from '@/lib/copilot/store-utils'
|
||||
import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-display-registry'
|
||||
import type { CopilotStore, CopilotToolCall } from '@/stores/panel/copilot/types'
|
||||
import { type SSEHandler, sseHandlers, updateStreamingMessage } from './handlers'
|
||||
import {
|
||||
type SSEHandler,
|
||||
sendAutoAcceptConfirmation,
|
||||
sseHandlers,
|
||||
updateStreamingMessage,
|
||||
} from './handlers'
|
||||
import type { ClientStreamingContext } from './types'
|
||||
|
||||
const logger = createLogger('CopilotClientSubagentHandlers')
|
||||
@@ -234,6 +239,12 @@ export const subAgentSSEHandlers: Record<string, SSEHandler> = {
|
||||
if (isPartial) {
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-allowed tools: send confirmation to the server so it can proceed
|
||||
// without waiting for the user to click "Allow".
|
||||
if (isAutoAllowed) {
|
||||
sendAutoAcceptConfirmation(id)
|
||||
}
|
||||
},
|
||||
|
||||
tool_result: (data, context, get, set) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
wasToolResultSeen,
|
||||
} from '@/lib/copilot/orchestrator/sse-utils'
|
||||
import {
|
||||
isIntegrationTool,
|
||||
isToolAvailableOnSimSide,
|
||||
markToolComplete,
|
||||
} from '@/lib/copilot/orchestrator/tool-executor'
|
||||
@@ -171,8 +172,10 @@ export const sseHandlers: Record<string, SSEHandler> = {
|
||||
|
||||
const isInterruptTool = isInterruptToolName(toolName)
|
||||
const isInteractive = options.interactive === true
|
||||
// Integration tools (user-installed) also require approval in interactive mode
|
||||
const needsApproval = isInterruptTool || isIntegrationTool(toolName)
|
||||
|
||||
if (isInterruptTool && isInteractive) {
|
||||
if (needsApproval && isInteractive) {
|
||||
const decision = await waitForToolDecision(
|
||||
toolCallId,
|
||||
options.timeout || STREAM_TIMEOUT_MS,
|
||||
@@ -372,6 +375,66 @@ export const subAgentHandlers: Record<string, SSEHandler> = {
|
||||
return
|
||||
}
|
||||
|
||||
// Integration tools (user-installed) require approval in interactive mode,
|
||||
// same as top-level interrupt tools.
|
||||
if (options.interactive === true && isIntegrationTool(toolName)) {
|
||||
const decision = await waitForToolDecision(
|
||||
toolCallId,
|
||||
options.timeout || STREAM_TIMEOUT_MS,
|
||||
options.abortSignal
|
||||
)
|
||||
if (decision?.status === 'accepted' || decision?.status === 'success') {
|
||||
await executeToolAndReport(toolCallId, context, execContext, options)
|
||||
return
|
||||
}
|
||||
if (decision?.status === 'rejected' || decision?.status === 'error') {
|
||||
toolCall.status = 'rejected'
|
||||
toolCall.endTime = Date.now()
|
||||
await markToolComplete(
|
||||
toolCall.id,
|
||||
toolCall.name,
|
||||
400,
|
||||
decision.message || 'Tool execution rejected',
|
||||
{ skipped: true, reason: 'user_rejected' }
|
||||
)
|
||||
markToolResultSeen(toolCall.id)
|
||||
await options?.onEvent?.({
|
||||
type: 'tool_result',
|
||||
toolCallId: toolCall.id,
|
||||
data: {
|
||||
id: toolCall.id,
|
||||
name: toolCall.name,
|
||||
success: false,
|
||||
result: { skipped: true, reason: 'user_rejected' },
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (decision?.status === 'background') {
|
||||
toolCall.status = 'skipped'
|
||||
toolCall.endTime = Date.now()
|
||||
await markToolComplete(
|
||||
toolCall.id,
|
||||
toolCall.name,
|
||||
202,
|
||||
decision.message || 'Tool execution moved to background',
|
||||
{ background: true }
|
||||
)
|
||||
markToolResultSeen(toolCall.id)
|
||||
await options?.onEvent?.({
|
||||
type: 'tool_result',
|
||||
toolCallId: toolCall.id,
|
||||
data: {
|
||||
id: toolCall.id,
|
||||
name: toolCall.name,
|
||||
success: true,
|
||||
result: { background: true },
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (options.autoExecuteTools !== false) {
|
||||
await executeToolAndReport(toolCallId, context, execContext, options)
|
||||
}
|
||||
|
||||
@@ -151,6 +151,19 @@ export function isToolAvailableOnSimSide(toolName: string): boolean {
|
||||
return !!getTool(resolvedToolName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a tool is a user-installed integration tool (e.g. Gmail, Slack).
|
||||
* These tools exist in the tool registry but are NOT copilot server tools or
|
||||
* known workflow manipulation tools. They should require user approval in
|
||||
* interactive mode.
|
||||
*/
|
||||
export function isIntegrationTool(toolName: string): boolean {
|
||||
if (SERVER_TOOLS.has(toolName)) return false
|
||||
if (toolName in SIM_WORKFLOW_TOOL_HANDLERS) return false
|
||||
const resolvedToolName = resolveToolId(toolName)
|
||||
return !!getTool(resolvedToolName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a tool server-side without calling internal routes.
|
||||
*/
|
||||
|
||||
@@ -401,6 +401,7 @@ export function createUserToolSchema(toolConfig: ToolConfig): ToolSchema {
|
||||
}
|
||||
|
||||
for (const [paramId, param] of Object.entries(toolConfig.params)) {
|
||||
if (!param) continue
|
||||
const visibility = param.visibility ?? 'user-or-llm'
|
||||
if (visibility === 'hidden') {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user