Fix superagent and autoallow integrations

This commit is contained in:
Siddharth Ganesan
2026-02-09 11:57:27 -08:00
parent 4698f731a9
commit 79af303265
7 changed files with 139 additions and 21 deletions

View File

@@ -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
}

View File

@@ -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),

View File

@@ -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 {

View File

@@ -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) => {

View File

@@ -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)
}

View File

@@ -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.
*/

View File

@@ -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