Fix always allow, credential validation

This commit is contained in:
Siddharth Ganesan
2026-01-22 11:57:57 -08:00
parent 66dfe2c6b2
commit 76dd4a0c95
4 changed files with 169 additions and 25 deletions

View File

@@ -1412,10 +1412,13 @@ function RunSkipButtons({
setIsProcessing(true)
setButtonsHidden(true)
try {
// Add to auto-allowed list first
// Add to auto-allowed list - this also executes all pending integration tools of this type
await addAutoAllowedTool(toolCall.name)
// Then execute
await handleRun(toolCall, setToolCallState, onStateChange, editedParams)
// For client tools with interrupts (not integration tools), we still need to call handleRun
// since executeIntegrationTool only works for server-side tools
if (!isIntegrationTool(toolCall.name)) {
await handleRun(toolCall, setToolCallState, onStateChange, editedParams)
}
} finally {
setIsProcessing(false)
actionInProgressRef.current = false
@@ -1438,10 +1441,10 @@ function RunSkipButtons({
if (buttonsHidden) return null
// Hide "Always Allow" for integration tools (only show for client tools with interrupts)
const showAlwaysAllow = !isIntegrationTool(toolCall.name)
// Show "Always Allow" for all tools that require confirmation
const showAlwaysAllow = true
// Standardized buttons for all interrupt tools: Allow, (Always Allow for client tools only), Skip
// Standardized buttons for all interrupt tools: Allow, Always Allow, Skip
return (
<div className='mt-[10px] flex gap-[6px]'>
<Button onClick={onRun} disabled={isProcessing} variant='tertiary'>

View File

@@ -2468,16 +2468,17 @@ async function validateWorkflowSelectorIds(
const result = await validateSelectorIds(selector.selectorType, selector.value, context)
if (result.invalid.length > 0) {
// Include warning info (like available credentials) in the error message for better LLM feedback
const warningInfo = result.warning ? `. ${result.warning}` : ''
errors.push({
blockId: selector.blockId,
blockType: selector.blockType,
field: selector.fieldName,
value: selector.value,
error: `Invalid ${selector.selectorType} ID(s): ${result.invalid.join(', ')} - ID(s) do not exist`,
error: `Invalid ${selector.selectorType} ID(s): ${result.invalid.join(', ')} - ID(s) do not exist or user doesn't have access${warningInfo}`,
})
}
if (result.warning) {
} else if (result.warning) {
// Log warnings that don't have errors (shouldn't happen for credentials but may for other selectors)
logger.warn(result.warning, {
blockId: selector.blockId,
fieldName: selector.fieldName,

View File

@@ -39,6 +39,31 @@ export async function validateSelectorIds(
.from(account)
.where(and(inArray(account.id, idsArray), eq(account.userId, context.userId)))
existingIds = results.map((r) => r.id)
// If any IDs are invalid, fetch user's available credentials to include in error message
const existingSet = new Set(existingIds)
const invalidIds = idsArray.filter((id) => !existingSet.has(id))
if (invalidIds.length > 0) {
// Fetch all of the user's credentials to provide helpful feedback
const allUserCredentials = await db
.select({ id: account.id, providerId: account.providerId })
.from(account)
.where(eq(account.userId, context.userId))
const availableCredentials = allUserCredentials
.map((c) => `${c.id} (${c.providerId})`)
.join(', ')
const noCredentialsMessage = 'User has no credentials configured.'
return {
valid: existingIds,
invalid: invalidIds,
warning:
allUserCredentials.length > 0
? `Available credentials for this user: ${availableCredentials}`
: noCredentialsMessage,
}
}
break
}

View File

@@ -1320,7 +1320,16 @@ const sseHandlers: Record<string, SSEHandler> = {
typeof def.hasInterrupt === 'function'
? !!def.hasInterrupt(args || {})
: !!def.hasInterrupt
if (!hasInterrupt && typeof def.execute === 'function') {
// Check if tool is auto-allowed - if so, execute even if it has an interrupt
const { autoAllowedTools } = get()
const isAutoAllowed = name ? autoAllowedTools.includes(name) : false
if ((!hasInterrupt || isAutoAllowed) && typeof def.execute === 'function') {
if (isAutoAllowed && hasInterrupt) {
logger.info('[toolCallsById] Auto-executing tool with interrupt (auto-allowed)', {
id,
name,
})
}
const ctx = createExecutionContext({ toolCallId: id, toolName: name || 'unknown_tool' })
// Defer executing transition by a tick to let pending render
setTimeout(() => {
@@ -1426,11 +1435,20 @@ const sseHandlers: Record<string, SSEHandler> = {
logger.warn('tool_call registry auto-exec check failed', { id, name, error: e })
}
// Class-based auto-exec for non-interrupt tools
// Class-based auto-exec for non-interrupt tools or auto-allowed tools
try {
const inst = getClientTool(id) as any
const hasInterrupt = !!inst?.getInterruptDisplays?.()
if (!hasInterrupt && typeof inst?.execute === 'function') {
// Check if tool is auto-allowed - if so, execute even if it has an interrupt
const { autoAllowedTools: classAutoAllowed } = get()
const isClassAutoAllowed = name ? classAutoAllowed.includes(name) : false
if ((!hasInterrupt || isClassAutoAllowed) && (typeof inst?.execute === 'function' || typeof inst?.handleAccept === 'function')) {
if (isClassAutoAllowed && hasInterrupt) {
logger.info('[toolCallsById] Auto-executing class tool with interrupt (auto-allowed)', {
id,
name,
})
}
setTimeout(() => {
// Guard against duplicate execution - check if already executing or terminal
const currentState = get().toolCallsById[id]?.state
@@ -1449,7 +1467,12 @@ const sseHandlers: Record<string, SSEHandler> = {
Promise.resolve()
.then(async () => {
await inst.execute(args || {})
// Use handleAccept for tools with interrupts, execute for others
if (hasInterrupt && typeof inst?.handleAccept === 'function') {
await inst.handleAccept(args || {})
} else {
await inst.execute(args || {})
}
// Success/error will be synced via registerToolStateSync
})
.catch(() => {
@@ -1474,20 +1497,35 @@ const sseHandlers: Record<string, SSEHandler> = {
}
} catch {}
// Integration tools: Stay in pending state until user confirms via buttons
// Integration tools: Check auto-allowed or stay in pending state until user confirms
// This handles tools like google_calendar_*, exa_*, gmail_read, etc. that aren't in the client registry
// Only relevant if mode is 'build' (agent)
const { mode, workflowId } = get()
const { mode, workflowId, autoAllowedTools, executeIntegrationTool } = get()
if (mode === 'build' && workflowId) {
// Check if tool was NOT found in client registry
const def = name ? getTool(name) : undefined
const inst = getClientTool(id) as any
if (!def && !inst && name) {
// Integration tools stay in pending state until user confirms
logger.info('[build mode] Integration tool awaiting user confirmation', {
id,
name,
})
// Check if this integration tool is auto-allowed - if so, execute it immediately
if (autoAllowedTools.includes(name)) {
logger.info('[build mode] Auto-executing integration tool (auto-allowed)', { id, name })
// Defer to allow pending state to render briefly
setTimeout(() => {
executeIntegrationTool(id).catch((err) => {
logger.error('[build mode] Auto-execute integration tool failed', {
id,
name,
error: err,
})
})
}, 0)
} else {
// Integration tools stay in pending state until user confirms
logger.info('[build mode] Integration tool awaiting user confirmation', {
id,
name,
})
}
}
}
},
@@ -1976,6 +2014,10 @@ const subAgentSSEHandlers: Record<string, SSEHandler> = {
}
// Execute client tools in parallel (non-blocking) - same pattern as main tool_call handler
// Check if tool is auto-allowed
const { autoAllowedTools: subAgentAutoAllowed } = get()
const isSubAgentAutoAllowed = name ? subAgentAutoAllowed.includes(name) : false
try {
const def = getTool(name)
if (def) {
@@ -1983,8 +2025,15 @@ const subAgentSSEHandlers: Record<string, SSEHandler> = {
typeof def.hasInterrupt === 'function'
? !!def.hasInterrupt(args || {})
: !!def.hasInterrupt
if (!hasInterrupt) {
// Auto-execute tools without interrupts - non-blocking
// Auto-execute if no interrupt OR if auto-allowed
if (!hasInterrupt || isSubAgentAutoAllowed) {
if (isSubAgentAutoAllowed && hasInterrupt) {
logger.info('[SubAgent] Auto-executing tool with interrupt (auto-allowed)', {
id,
name,
})
}
// Auto-execute tools - non-blocking
const ctx = createExecutionContext({ toolCallId: id, toolName: name })
Promise.resolve()
.then(() => def.execute(ctx, args || {}))
@@ -2001,9 +2050,22 @@ const subAgentSSEHandlers: Record<string, SSEHandler> = {
const instance = getClientTool(id)
if (instance) {
const hasInterruptDisplays = !!instance.getInterruptDisplays?.()
if (!hasInterruptDisplays) {
// Auto-execute if no interrupt OR if auto-allowed
if (!hasInterruptDisplays || isSubAgentAutoAllowed) {
if (isSubAgentAutoAllowed && hasInterruptDisplays) {
logger.info('[SubAgent] Auto-executing class tool with interrupt (auto-allowed)', {
id,
name,
})
}
Promise.resolve()
.then(() => instance.execute(args || {}))
.then(() => {
// Use handleAccept for tools with interrupts, execute for others
if (hasInterruptDisplays && typeof instance.handleAccept === 'function') {
return instance.handleAccept(args || {})
}
return instance.execute(args || {})
})
.catch((execErr: any) => {
logger.error('[SubAgent] Class tool execution failed', {
id,
@@ -3676,6 +3738,19 @@ export const useCopilotStore = create<CopilotStore>()(
const { id, name, params } = toolCall
// Guard against double execution - skip if already executing or in terminal state
if (
toolCall.state === ClientToolCallState.executing ||
isTerminalState(toolCall.state)
) {
logger.info('[executeIntegrationTool] Skipping - already executing or terminal', {
id,
name,
state: toolCall.state,
})
return
}
// Set to executing state
const executingMap = { ...get().toolCallsById }
executingMap[id] = {
@@ -3824,6 +3899,46 @@ export const useCopilotStore = create<CopilotStore>()(
const data = await res.json()
set({ autoAllowedTools: data.autoAllowedTools || [] })
logger.info('[AutoAllowedTools] Added tool', { toolId })
// Auto-execute all pending tools of the same type
const { toolCallsById, executeIntegrationTool } = get()
const pendingToolCalls = Object.values(toolCallsById).filter(
(tc) => tc.name === toolId && tc.state === ClientToolCallState.pending
)
if (pendingToolCalls.length > 0) {
const isIntegrationTool = !CLASS_TOOL_METADATA[toolId]
logger.info('[AutoAllowedTools] Auto-executing pending tools', {
toolId,
count: pendingToolCalls.length,
isIntegrationTool,
})
for (const tc of pendingToolCalls) {
if (isIntegrationTool) {
// Integration tools use executeIntegrationTool
executeIntegrationTool(tc.id).catch((err) => {
logger.error('[AutoAllowedTools] Auto-execute pending integration tool failed', {
toolCallId: tc.id,
toolId,
error: err,
})
})
} else {
// Client tools with interrupts use handleAccept
const inst = getClientTool(tc.id) as any
if (inst && typeof inst.handleAccept === 'function') {
Promise.resolve()
.then(() => inst.handleAccept(tc.params || {}))
.catch((err: any) => {
logger.error('[AutoAllowedTools] Auto-execute pending client tool failed', {
toolCallId: tc.id,
toolId,
error: err,
})
})
}
}
}
}
}
} catch (err) {
logger.error('[AutoAllowedTools] Failed to add tool', { toolId, error: err })