mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-07 08:25:23 +02:00
feat: implement tool approval checks for agent tool calls
Ports the tool approval feature from aron/tool-approval branch onto the latest codebase. Adds manual user approval flow for tool calls before execution, configurable via librechat.yaml toolApproval config. Key changes: - Add TToolApproval schema to data-provider config (required/excluded patterns) - Add approval.ts utilities (requiresApproval, matchesPattern, getToolServerName) - Add MCPToolCallValidationHandler for flow-based approval via FlowStateManager - Wrap non-MCP tools with approval in ToolService.loadAgentTools - Add MCP tool validation in MCP.js createToolInstance - Handle native Anthropic web search approval in callbacks.js - Disable native web_search when approval required (OpenAI initialize) - Add validation SSE delta handling in useStepHandler - Add approve/reject UI in ToolCall.tsx with confirm/reject API calls - Add validation routes: POST /api/mcp/validation/confirm|reject/:id - Add i18n keys for approval UI - Add toolApproval example config in librechat.example.yaml Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6ecd1b510f
commit
301ba801f4
19 changed files with 720 additions and 5 deletions
|
|
@ -12,6 +12,8 @@ const {
|
|||
const {
|
||||
sendEvent,
|
||||
getToolkitKey,
|
||||
requiresApproval,
|
||||
getToolServerName,
|
||||
getUserMCPAuthMap,
|
||||
loadToolDefinitions,
|
||||
GenerationJobManager,
|
||||
|
|
@ -20,6 +22,7 @@ const {
|
|||
buildImageToolContext,
|
||||
buildToolClassification,
|
||||
buildOAuthToolCallName,
|
||||
MCPToolCallValidationHandler,
|
||||
} = require('@librechat/api');
|
||||
const {
|
||||
Time,
|
||||
|
|
@ -810,6 +813,94 @@ async function loadToolDefinitionsWrapper({ req, res, agent, streamId = null, to
|
|||
* @param {ServerRequest} params.req - The request object
|
||||
* @param {ServerResponse} params.res - The response object
|
||||
* @param {Object} params.agent - The agent configuration
|
||||
/**
|
||||
* Wraps a tool with approval validation flow.
|
||||
* The wrapped tool sends an SSE event and waits for user approval before executing.
|
||||
* @param {Object} params
|
||||
* @param {Object} params.tool - The tool to wrap
|
||||
* @param {ServerResponse} params.res - The response object for SSE
|
||||
* @param {string|null} params.streamId - Stream ID for resumable mode
|
||||
* @returns {Object} The wrapped tool
|
||||
*/
|
||||
function wrapToolWithApproval({ tool, res, streamId }) {
|
||||
const originalCall = tool._call.bind(tool);
|
||||
const toolName = tool.name;
|
||||
const serverName = getToolServerName(toolName);
|
||||
|
||||
tool._call = async (toolArguments, runManager, parentConfig) => {
|
||||
const config = parentConfig;
|
||||
const userId = config?.configurable?.user?.id || config?.configurable?.user_id;
|
||||
|
||||
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
||||
const flowManager = getFlowStateManager(flowsCache);
|
||||
const derivedSignal = config?.signal ? AbortSignal.any([config.signal]) : undefined;
|
||||
|
||||
const { args: _args, stepId, ...toolCall } = config?.toolCall ?? {};
|
||||
|
||||
const { validationId, flowMetadata } =
|
||||
await MCPToolCallValidationHandler.initiateValidationFlow(
|
||||
userId,
|
||||
serverName,
|
||||
toolName,
|
||||
typeof toolArguments === 'string' ? { input: toolArguments } : toolArguments,
|
||||
);
|
||||
|
||||
const validationData = {
|
||||
id: stepId,
|
||||
delta: {
|
||||
type: StepTypes.TOOL_CALLS,
|
||||
tool_calls: [{ ...toolCall, args: '' }],
|
||||
validation: validationId,
|
||||
expires_at: Date.now() + Time.TEN_MINUTES,
|
||||
},
|
||||
};
|
||||
|
||||
if (streamId) {
|
||||
await GenerationJobManager.emitChunk(streamId, {
|
||||
event: GraphEvents.ON_RUN_STEP_DELTA,
|
||||
data: validationData,
|
||||
});
|
||||
} else {
|
||||
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data: validationData });
|
||||
}
|
||||
|
||||
const validationFlowType = MCPToolCallValidationHandler.getFlowType();
|
||||
try {
|
||||
await flowManager.createFlow(validationId, validationFlowType, flowMetadata, derivedSignal);
|
||||
|
||||
const successData = {
|
||||
id: stepId,
|
||||
delta: {
|
||||
type: StepTypes.TOOL_CALLS,
|
||||
tool_calls: [{ ...toolCall }],
|
||||
},
|
||||
};
|
||||
if (streamId) {
|
||||
await GenerationJobManager.emitChunk(streamId, {
|
||||
event: GraphEvents.ON_RUN_STEP_DELTA,
|
||||
data: successData,
|
||||
});
|
||||
} else {
|
||||
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data: successData });
|
||||
}
|
||||
} catch (validationError) {
|
||||
throw new Error(
|
||||
`Tool call validation required for ${toolName}. User rejected or validation timed out.`,
|
||||
);
|
||||
}
|
||||
|
||||
return await originalCall(toolArguments, runManager, parentConfig);
|
||||
};
|
||||
|
||||
tool.requiresApproval = true;
|
||||
return tool;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params - Run params containing user and request information.
|
||||
* @param {ServerRequest} params.req - The server request
|
||||
* @param {ServerResponse} params.res - The server response
|
||||
* @param {Agent} params.agent - The Agent
|
||||
* @param {AbortSignal} [params.signal] - Abort signal
|
||||
* @param {Object} [params.tool_resources] - Tool resources
|
||||
* @param {string} [params.openAIApiKey] - OpenAI API key
|
||||
|
|
@ -933,9 +1024,16 @@ async function loadAgentTools({
|
|||
loadAuthValues,
|
||||
});
|
||||
|
||||
const toolApprovalConfig = appConfig.endpoints?.[EModelEndpoint.agents]?.toolApproval;
|
||||
const agentTools = [];
|
||||
for (let i = 0; i < loadedTools.length; i++) {
|
||||
const tool = loadedTools[i];
|
||||
let tool = loadedTools[i];
|
||||
|
||||
const needsApproval = requiresApproval(tool.name, toolApprovalConfig);
|
||||
if (res && needsApproval && tool.mcp !== true) {
|
||||
tool = wrapToolWithApproval({ tool, res, streamId });
|
||||
}
|
||||
|
||||
if (tool.name && (tool.name === Tools.execute_code || tool.name === Tools.file_search)) {
|
||||
agentTools.push(tool);
|
||||
continue;
|
||||
|
|
@ -945,7 +1043,7 @@ async function loadAgentTools({
|
|||
continue;
|
||||
}
|
||||
|
||||
if (tool.mcp === true) {
|
||||
if (tool.mcp === true || tool.requiresApproval === true) {
|
||||
agentTools.push(tool);
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue