Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion api/server/controllers/agents/openai.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,12 @@ const OpenAIChatCompletionController = async (req, res) => {
tool_resources: primaryConfig.tool_resources,
actionsEnabled: primaryConfig.actionsEnabled,
});
return enrichWithSkillConfigurable(result, req, primaryConfig.accessibleSkillIds);
return enrichWithSkillConfigurable(
result,
req,
primaryConfig.accessibleSkillIds,
enabledCapabilities.has(AgentCapabilities.execute_code),
);
},
toolEndCallback,
...getSkillToolDeps(),
Expand Down
14 changes: 12 additions & 2 deletions api/server/controllers/agents/responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,12 @@ const createResponse = async (req, res) => {
tool_resources: primaryConfig.tool_resources,
actionsEnabled: primaryConfig.actionsEnabled,
});
return enrichWithSkillConfigurable(result, req, primaryConfig.accessibleSkillIds);
return enrichWithSkillConfigurable(
result,
req,
primaryConfig.accessibleSkillIds,
enabledCapabilities.has(AgentCapabilities.execute_code),
);
},
toolEndCallback,
...getSkillToolDeps(),
Expand Down Expand Up @@ -655,7 +660,12 @@ const createResponse = async (req, res) => {
tool_resources: primaryConfig.tool_resources,
actionsEnabled: primaryConfig.actionsEnabled,
});
return enrichWithSkillConfigurable(result, req, primaryConfig.accessibleSkillIds);
return enrichWithSkillConfigurable(
result,
req,
primaryConfig.accessibleSkillIds,
enabledCapabilities.has(AgentCapabilities.execute_code),
);
},
toolEndCallback,
...getSkillToolDeps(),
Expand Down
26 changes: 5 additions & 21 deletions api/server/services/Endpoints/agents/initialize.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { logger } = require('@librechat/data-schemas');
const { EnvVar, createContentAggregator } = require('@librechat/agents');
const { createContentAggregator } = require('@librechat/agents');
const {
scopeSkillIds,
loadSkillStates,
Expand All @@ -26,7 +26,6 @@ const {
getDefaultHandlers,
} = require('~/server/controllers/agents/callbacks');
const { loadAgentTools, loadToolsForExecution } = require('~/server/services/ToolService');
const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions');
const { getSkillToolDeps, enrichWithSkillConfigurable } = require('./skillDeps');
const { getModelsConfig } = require('~/server/controllers/ModelController');
Expand Down Expand Up @@ -118,6 +117,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
* - the agent has stored skills (scoped by scopeSkillIds later). */
const enabledCapabilities = new Set(appConfig?.endpoints?.[EModelEndpoint.agents]?.capabilities);
const skillsCapabilityEnabled = enabledCapabilities.has(AgentCapabilities.skills);
const codeEnvAvailable = enabledCapabilities.has(AgentCapabilities.execute_code);
const ephemeralSkillsToggle = req.body?.ephemeralAgent?.skills === true;

const accessibleSkillIds = skillsCapabilityEnabled
Expand All @@ -136,21 +136,6 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
accessibleSkillIds,
});

// Resolve code API key once for the entire run (shared by primeInvokedSkills
// and enrichWithSkillConfigurable) to avoid redundant auth lookups.
let codeApiKey;
if (skillsCapabilityEnabled && enabledCapabilities.has(AgentCapabilities.execute_code)) {
try {
const authValues = await loadAuthValues({
userId: req.user.id,
authFields: [EnvVar.CODE_API_KEY],
});
codeApiKey = authValues[EnvVar.CODE_API_KEY];
} catch {
// non-fatal β€” primeInvokedSkills and enrichWithSkillConfigurable will work without it
}
}

/**
* Agent context store - populated after initialization, accessed by callback via closure.
* Maps agentId -> { userMCPAuthMap, agent, tool_resources, toolRegistry, openAIApiKey }
Expand Down Expand Up @@ -184,7 +169,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
});

logger.debug(`[ON_TOOL_EXECUTE] loaded ${result.loadedTools?.length ?? 0} tools`);
return enrichWithSkillConfigurable(result, req, ctx.accessibleSkillIds, codeApiKey);
return enrichWithSkillConfigurable(result, req, ctx.accessibleSkillIds, codeEnvAvailable);
},
toolEndCallback,
...getSkillToolDeps(),
Expand Down Expand Up @@ -254,7 +239,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
accessibleSkillIds,
ephemeralSkillsToggle ? undefined : primaryAgent.skills,
),
codeEnvAvailable: enabledCapabilities.has(AgentCapabilities.execute_code),
codeEnvAvailable,
skillStates,
defaultActiveOnShare,
},
Expand Down Expand Up @@ -512,8 +497,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
req,
payload,
accessibleSkillIds,
codeApiKey,
loadAuthValues,
codeEnvAvailable,
...getSkillToolDeps(),
})
: undefined;
Expand Down
21 changes: 1 addition & 20 deletions api/server/services/Endpoints/agents/skillDeps.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { batchUploadCodeEnvFiles } = require('~/server/services/Files/Code/crud');
const { getSessionInfo, checkIfActive } = require('~/server/services/Files/Code/process');
const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { enrichWithSkillConfigurable } = require('@librechat/api');
const db = require('~/models');

Expand All @@ -22,22 +21,4 @@ function getSkillToolDeps() {
return skillToolDeps;
}

/**
* Wraps the TS enrichWithSkillConfigurable with the CJS loadAuthValues dependency.
* @param {object} result - The result from loadToolsForExecution
* @param {object} req - The Express request object
* @param {Array} accessibleSkillIds - Pre-computed accessible skill IDs
* @param {string} [preResolvedCodeApiKey] - Pre-resolved code API key (skips redundant lookup)
* @returns {Promise<object>} Augmented result with skill configurable
*/
function enrichConfigurable(result, req, accessibleSkillIds, preResolvedCodeApiKey) {
return enrichWithSkillConfigurable(
result,
req,
accessibleSkillIds,
loadAuthValues,
preResolvedCodeApiKey,
);
}

module.exports = { getSkillToolDeps, enrichWithSkillConfigurable: enrichConfigurable };
module.exports = { getSkillToolDeps, enrichWithSkillConfigurable };
33 changes: 6 additions & 27 deletions api/server/services/ToolService.js
Original file line number Diff line number Diff line change
Expand Up @@ -1273,34 +1273,13 @@ async function loadToolsForExecution({
const isBashTool = toolNames.includes(AgentConstants.BASH_TOOL);
if (isBashTool) {
try {
const authValues = await loadAuthValues({
userId: req.user.id,
authFields: [EnvVar.CODE_API_KEY],
});
const codeApiKey = authValues[EnvVar.CODE_API_KEY];

if (codeApiKey) {
const bashTool = createBashExecutionTool({ apiKey: codeApiKey });
allLoadedTools.push(bashTool);
} else {
logger.debug('[loadToolsForExecution] bash_tool requested but CODE_API_KEY not available');
allLoadedTools.push(
toolFn(
async () => [
'Code execution is not available. Use the read_file tool instead.',
undefined,
],
{
name: AgentConstants.BASH_TOOL,
description: 'Bash execution (unavailable - no code API key configured)',
schema: { type: 'object', properties: { command: { type: 'string' } } },
responseFormat: AgentConstants.CONTENT_AND_ARTIFACT,
},
),
);
}
const bashTool = createBashExecutionTool({});
allLoadedTools.push(bashTool);
} catch (error) {
logger.error('[loadToolsForExecution] Error creating bash tool:', error);
logger.error(
'[loadToolsForExecution] Failed to create bash_tool β€” is LIBRECHAT_CODE_API_KEY set in the server environment?',
error,
);
}
}

Expand Down
10 changes: 7 additions & 3 deletions packages/api/src/agents/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { logger } from '@librechat/data-schemas';
import { GraphEvents, Constants, CODE_EXECUTION_TOOLS } from '@librechat/agents';
import { EnvVar, GraphEvents, Constants, CODE_EXECUTION_TOOLS } from '@librechat/agents';
import type {
LCTool,
EventHandler,
Expand Down Expand Up @@ -446,15 +446,19 @@ async function handleSkillToolCall(
| { session_id: string; files: Array<{ id: string; session_id: string; name: string }> }
| undefined;

// Prime skill files to code env when the skill has bundled files
// Prime skill files to code env β€” only when the `execute_code` capability
// is enabled for this run. The flag is threaded via configurable upstream
// so this gate cannot be bypassed by a stray env var.
const codeEnvAvailable = mergedConfigurable?.codeEnvAvailable === true;
if (
codeEnvAvailable &&
skill.fileCount > 0 &&
req &&
listSkillFiles &&
getStrategyFunctions &&
batchUploadCodeEnvFiles
) {
const codeApiKey = (mergedConfigurable?.codeApiKey as string) ?? '';
const codeApiKey = process.env[EnvVar.CODE_API_KEY] ?? '';
if (codeApiKey) {
Comment on lines +461 to 462
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce execute_code capability before priming skill files

This path now primes skill files whenever LIBRECHAT_CODE_API_KEY is present in the server environment, but it no longer checks whether code execution is enabled for the run/agent. In deployments where skills are enabled but execute_code is intentionally disabled, invoking a file-backed skill will still upload files to the code sandbox, causing unintended data egress and background code-env activity. Please gate this branch on the same execute-code capability signal used when registering bash_tool.

Useful? React with πŸ‘Β / πŸ‘Ž.

try {
const skillFiles = await listSkillFiles(skill._id);
Expand Down
56 changes: 56 additions & 0 deletions packages/api/src/agents/skillConfigurable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { enrichWithSkillConfigurable } from './skillConfigurable';

describe('enrichWithSkillConfigurable', () => {
const req = { user: { id: 'user-1' } };
const accessibleSkillIds = ['skill-a', 'skill-b'];

it('augments configurable with req, accessibleSkillIds, and codeEnvAvailable', () => {
const result = enrichWithSkillConfigurable(
{ loadedTools: [], configurable: { other: 'value' } },
req,
accessibleSkillIds,
true,
);

expect(result.configurable).toEqual({
other: 'value',
req,
codeEnvAvailable: true,
accessibleSkillIds,
});
});

it('propagates codeEnvAvailable=false verbatim (not coerced)', () => {
const result = enrichWithSkillConfigurable(
{ loadedTools: [], configurable: {} },
req,
accessibleSkillIds,
false,
);

expect(result.configurable.codeEnvAvailable).toBe(false);
});

it('does not inject a codeApiKey key (per-user lookup removed)', () => {
const result = enrichWithSkillConfigurable(
{ loadedTools: [], configurable: {} },
req,
accessibleSkillIds,
true,
);

expect(result.configurable).not.toHaveProperty('codeApiKey');
});

it('preserves loadedTools unchanged', () => {
const tools = [{ name: 'x' }];
const result = enrichWithSkillConfigurable(
{ loadedTools: tools, configurable: undefined },
req,
accessibleSkillIds,
false,
);

expect(result.loadedTools).toBe(tools);
});
});
39 changes: 10 additions & 29 deletions packages/api/src/agents/skillConfigurable.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,23 @@
import { EnvVar } from '@librechat/agents';
import { logger } from '@librechat/data-schemas';

/**
* Augments a loadTools result with skill-specific configurable properties.
* Loads the code API key and merges it with accessibleSkillIds and the request object.
* Augments a loadTools result with skill-specific configurable properties:
* the request object, pre-computed accessible skill IDs, and the
* `codeEnvAvailable` capability flag. Downstream skill consumers
* (skill-tool handler, primeInvokedSkills) gate sandbox uploads on
* `codeEnvAvailable` β€” not on API-key presence β€” so skill file priming
* never runs when `execute_code` is disabled for the agent.
*/
export async function enrichWithSkillConfigurable(
export function enrichWithSkillConfigurable(
result: { loadedTools: unknown[]; configurable?: Record<string, unknown> },
req: { user?: { id?: string } },
accessibleSkillIds: unknown[],
loadAuthValues: (params: {
userId: string;
authFields: string[];
}) => Promise<Record<string, string>>,
/** Pre-resolved code API key. When provided, loadAuthValues is skipped. */
preResolvedCodeApiKey?: string,
): Promise<{ loadedTools: unknown[]; configurable: Record<string, unknown> }> {
let codeApiKey: string | undefined = preResolvedCodeApiKey;
if (!codeApiKey) {
try {
const authValues = await loadAuthValues({
userId: req.user?.id ?? '',
authFields: [EnvVar.CODE_API_KEY],
});
codeApiKey = authValues[EnvVar.CODE_API_KEY];
} catch (err) {
logger.debug(
'[enrichWithSkillConfigurable] loadAuthValues failed:',
err instanceof Error ? err.message : err,
);
}
}
codeEnvAvailable: boolean,
): { loadedTools: unknown[]; configurable: Record<string, unknown> } {
return {
...result,
configurable: {
...result.configurable,
req,
codeApiKey,
codeEnvAvailable,
accessibleSkillIds,
},
};
Expand Down
Loading
Loading