mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-31 08:56:48 +00:00
🔀 fix: Reconcile Agent Action Credential Merges (#13559)
* fix: Refine Agent Action Updates * fix: Format Action Update Helper * fix: Refine Agent Action Update Handling * fix: Move Agent Action Update Planning * fix: Sort Action Update Imports * chore: Reorder imports in actions.js for clarity
This commit is contained in:
parent
8c71dbcb32
commit
07af6ee288
11 changed files with 1007 additions and 47 deletions
|
|
@ -3,8 +3,12 @@ const { nanoid } = require('nanoid');
|
|||
const { logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
generateCheckAccess,
|
||||
planAgentActionUpdate,
|
||||
isActionDomainAllowed,
|
||||
legacyActionDomainEncode,
|
||||
validateActionOAuthMetadata,
|
||||
ACTION_CREDENTIAL_REFRESH_MESSAGE,
|
||||
buildActionOAuthTokenDeleteQueries,
|
||||
} = require('@librechat/api');
|
||||
const {
|
||||
Permissions,
|
||||
|
|
@ -16,17 +20,19 @@ const {
|
|||
validateActionDomain,
|
||||
validateAndParseOpenAPISpec,
|
||||
} = require('librechat-data-provider');
|
||||
const {
|
||||
legacyDomainEncode,
|
||||
encryptMetadata,
|
||||
domainParser,
|
||||
} = require('~/server/services/ActionService');
|
||||
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
|
||||
const { findAccessibleResources } = require('~/server/services/PermissionService');
|
||||
const db = require('~/models');
|
||||
const { canAccessAgentResource } = require('~/server/middleware');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
async function deleteActionOAuthTokens(action_id) {
|
||||
await Promise.all(
|
||||
buildActionOAuthTokenDeleteQueries(action_id).map((query) => db.deleteTokens(query)),
|
||||
);
|
||||
}
|
||||
|
||||
const checkAgentCreate = generateCheckAccess({
|
||||
permissionType: PermissionTypes.AGENTS,
|
||||
permissions: [Permissions.USE, Permissions.CREATE],
|
||||
|
|
@ -92,7 +98,7 @@ router.post(
|
|||
return res.status(400).json({ message: 'No functions provided' });
|
||||
}
|
||||
|
||||
let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
|
||||
const metadata = await encryptMetadata(removeNullishValues(_metadata, true));
|
||||
const appConfig = req.config;
|
||||
|
||||
// SECURITY: Validate the OpenAPI spec and extract the server URL
|
||||
|
|
@ -135,15 +141,16 @@ router.post(
|
|||
return res.status(400).json({ message: 'No domain provided' });
|
||||
}
|
||||
|
||||
const legacyDomain = legacyDomainEncode(metadata.domain);
|
||||
const legacyDomain = legacyActionDomainEncode(metadata.domain);
|
||||
|
||||
const action_id = _action_id ?? nanoid();
|
||||
const requestedActionId = _action_id;
|
||||
const action_id = requestedActionId ?? nanoid();
|
||||
const initialPromises = [];
|
||||
|
||||
// Permissions already validated by middleware - load agent directly
|
||||
initialPromises.push(db.getAgent({ id: agent_id }));
|
||||
if (_action_id) {
|
||||
initialPromises.push(db.getActions({ action_id }, true));
|
||||
if (requestedActionId) {
|
||||
initialPromises.push(db.getActions({ action_id: requestedActionId }, true));
|
||||
}
|
||||
|
||||
/** @type {[Agent, [Action|undefined]]} */
|
||||
|
|
@ -152,53 +159,51 @@ router.post(
|
|||
return res.status(404).json({ message: 'Agent not found for adding action' });
|
||||
}
|
||||
|
||||
if (actions_result && actions_result.length) {
|
||||
const action = actions_result[0];
|
||||
if (action.agent_id !== agent_id) {
|
||||
const storedAction = actions_result?.[0];
|
||||
if (storedAction) {
|
||||
if (storedAction.agent_id !== agent_id) {
|
||||
return res.status(403).json({ message: 'Action does not belong to this agent' });
|
||||
}
|
||||
metadata = { ...action.metadata, ...metadata };
|
||||
}
|
||||
|
||||
const { actions: agentActions = [], tools: agentTools = [], author: agent_author } = agent;
|
||||
const plannedUpdate = planAgentActionUpdate({
|
||||
agentActions,
|
||||
agentTools,
|
||||
incomingFunctions: functions,
|
||||
incomingMetadata: metadata,
|
||||
actionId: action_id,
|
||||
requestedActionId,
|
||||
encodedDomain,
|
||||
legacyDomain,
|
||||
previousLegacyDomain: legacyActionDomainEncode(storedAction?.metadata?.domain),
|
||||
storedAction,
|
||||
});
|
||||
|
||||
if (plannedUpdate.requiresCredentialRefresh) {
|
||||
return res.status(400).json({
|
||||
message: ACTION_CREDENTIAL_REFRESH_MESSAGE,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await validateActionOAuthMetadata(metadata.auth, appConfig?.actions?.allowedAddresses);
|
||||
await validateActionOAuthMetadata(
|
||||
plannedUpdate.metadata.auth,
|
||||
appConfig?.actions?.allowedAddresses,
|
||||
);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ message: error.message });
|
||||
}
|
||||
|
||||
const { actions: _actions = [], author: agent_author } = agent ?? {};
|
||||
const actions = [];
|
||||
for (const action of _actions) {
|
||||
const [_action_domain, current_action_id] = action.split(actionDelimiter);
|
||||
if (current_action_id === action_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
actions.push(action);
|
||||
if (plannedUpdate.deleteOAuthTokens && requestedActionId) {
|
||||
// Keep the callback URL stable while preventing old OAuth tokens from following a new target.
|
||||
await deleteActionOAuthTokens(requestedActionId);
|
||||
}
|
||||
|
||||
actions.push(`${encodedDomain}${actionDelimiter}${action_id}`);
|
||||
|
||||
/** @type {string[]}} */
|
||||
const { tools: _tools = [] } = agent;
|
||||
|
||||
const shouldRemoveAgentTool = (tool) => {
|
||||
if (!tool) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
tool.includes(encodedDomain) || tool.includes(legacyDomain) || tool.includes(action_id)
|
||||
);
|
||||
};
|
||||
|
||||
const tools = _tools
|
||||
.filter((tool) => !shouldRemoveAgentTool(tool))
|
||||
.concat(functions.map((tool) => `${tool.function.name}${actionDelimiter}${encodedDomain}`));
|
||||
|
||||
// Force version update since actions are changing
|
||||
const updatedAgent = await db.updateAgent(
|
||||
{ id: agent_id },
|
||||
{ tools, actions },
|
||||
{ tools: plannedUpdate.tools, actions: plannedUpdate.actions },
|
||||
{
|
||||
updatingUserId: req.user.id,
|
||||
forceVersion: true,
|
||||
|
|
@ -206,14 +211,21 @@ router.post(
|
|||
);
|
||||
|
||||
// Only update user field for new actions
|
||||
const actionUpdateData = { metadata, agent_id };
|
||||
const actionUpdateData = {
|
||||
action_id: plannedUpdate.actionId,
|
||||
metadata: plannedUpdate.metadata,
|
||||
agent_id,
|
||||
};
|
||||
if (!actions_result || !actions_result.length) {
|
||||
// For new actions, use the agent owner's user ID
|
||||
actionUpdateData.user = agent_author || req.user.id;
|
||||
}
|
||||
|
||||
/** @type {[Action]} */
|
||||
const updatedAction = await db.updateAction({ action_id, agent_id }, actionUpdateData);
|
||||
/** @type {Action} */
|
||||
const updatedAction = await db.updateAction(
|
||||
{ action_id: requestedActionId ?? action_id, agent_id },
|
||||
actionUpdateData,
|
||||
);
|
||||
|
||||
const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];
|
||||
for (let field of sensitiveFields) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue