mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-03 22:32:42 +00:00
fix: encrypt Langfuse agent secrets
This commit is contained in:
parent
dfc297ce2d
commit
0c2fdc92cd
4 changed files with 240 additions and 54 deletions
|
|
@ -1,7 +1,7 @@
|
|||
const { z } = require('zod');
|
||||
const fs = require('fs').promises;
|
||||
const { nanoid } = require('nanoid');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { logger, encryptV2 } = require('@librechat/data-schemas');
|
||||
const {
|
||||
refreshS3Url,
|
||||
agentCreateSchema,
|
||||
|
|
@ -64,8 +64,22 @@ const toPlainObject = (value) =>
|
|||
value && typeof value.toObject === 'function' ? value.toObject() : value;
|
||||
|
||||
const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0;
|
||||
const ENCRYPTED_V2_VALUE = /^[a-f0-9]{32}:[a-f0-9]+$/i;
|
||||
|
||||
const normalizeLangfuseConfig = (incoming, existing) => {
|
||||
const encryptSensitiveValue = async (value) => encryptV2(encodeURIComponent(value));
|
||||
|
||||
const normalizeLangfuseSecret = async (value, options = {}) => {
|
||||
if (!isNonEmptyString(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (options.preserveEncrypted === true && ENCRYPTED_V2_VALUE.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
return await encryptSensitiveValue(trimmed);
|
||||
};
|
||||
|
||||
const normalizeLangfuseConfig = async (incoming, existing, options = {}) => {
|
||||
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) {
|
||||
return incoming;
|
||||
}
|
||||
|
|
@ -84,9 +98,13 @@ const normalizeLangfuseConfig = (incoming, existing) => {
|
|||
}
|
||||
|
||||
if (isNonEmptyString(incoming.secretKey)) {
|
||||
normalized.secretKey = incoming.secretKey.trim();
|
||||
normalized.secretKey = await normalizeLangfuseSecret(incoming.secretKey, {
|
||||
preserveEncrypted: options.preserveIncomingEncrypted === true,
|
||||
});
|
||||
} else if (isNonEmptyString(existingConfig.secretKey)) {
|
||||
normalized.secretKey = existingConfig.secretKey;
|
||||
normalized.secretKey = await normalizeLangfuseSecret(existingConfig.secretKey, {
|
||||
preserveEncrypted: true,
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
||||
|
|
@ -349,7 +367,7 @@ const createAgentHandler = async (req, res) => {
|
|||
}
|
||||
|
||||
if (agentData.langfuse) {
|
||||
agentData.langfuse = normalizeLangfuseConfig(agentData.langfuse);
|
||||
agentData.langfuse = await normalizeLangfuseConfig(agentData.langfuse);
|
||||
if (!agentData.langfuse) {
|
||||
delete agentData.langfuse;
|
||||
}
|
||||
|
|
@ -628,7 +646,10 @@ const updateAgentHandler = async (req, res) => {
|
|||
}
|
||||
|
||||
if (updateData.langfuse) {
|
||||
updateData.langfuse = normalizeLangfuseConfig(updateData.langfuse, existingAgent.langfuse);
|
||||
updateData.langfuse = await normalizeLangfuseConfig(
|
||||
updateData.langfuse,
|
||||
existingAgent.langfuse,
|
||||
);
|
||||
if (!updateData.langfuse) {
|
||||
delete updateData.langfuse;
|
||||
}
|
||||
|
|
@ -1120,7 +1141,7 @@ const uploadAgentAvatarHandler = async (req, res) => {
|
|||
logger.error('[/:agent_id/avatar] Error invalidating avatar refresh cache', cacheErr);
|
||||
}
|
||||
|
||||
res.status(201).json(updatedAgent);
|
||||
res.status(201).json(redactLangfuseSecret(updatedAgent));
|
||||
} catch (error) {
|
||||
const message = 'An error occurred while updating the Agent Avatar';
|
||||
logger.error(
|
||||
|
|
@ -1204,6 +1225,15 @@ const revertAgentVersionHandler = async (req, res) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (updatedAgent.langfuse) {
|
||||
const normalizedLangfuse = await normalizeLangfuseConfig(updatedAgent.langfuse, undefined, {
|
||||
preserveIncomingEncrypted: true,
|
||||
});
|
||||
if (normalizedLangfuse) {
|
||||
revertUpdates.langfuse = normalizedLangfuse;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(revertUpdates).length > 0) {
|
||||
updatedAgent = await db.updateAgent({ id }, revertUpdates, { updatingUserId: req.user.id });
|
||||
}
|
||||
|
|
@ -1216,7 +1246,7 @@ const revertAgentVersionHandler = async (req, res) => {
|
|||
delete updatedAgent.author;
|
||||
}
|
||||
|
||||
return res.json(updatedAgent);
|
||||
return res.json(redactLangfuseSecret(updatedAgent));
|
||||
} catch (error) {
|
||||
logger.error('[/agents/:id/revert] Error reverting Agent version', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
process.env.CREDS_KEY =
|
||||
process.env.CREDS_KEY?.length === 64
|
||||
? process.env.CREDS_KEY
|
||||
: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||
process.env.CREDS_IV =
|
||||
process.env.CREDS_IV?.length === 32 ? process.env.CREDS_IV : '0123456789abcdef0123456789abcdef';
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const fs = require('fs').promises;
|
||||
const { nanoid } = require('nanoid');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { agentSchema, fileSchema } = require('@librechat/data-schemas');
|
||||
const { agentSchema, fileSchema, encryptV2, decryptV2 } = require('@librechat/data-schemas');
|
||||
const { FileSources, PermissionBits } = require('librechat-data-provider');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
|
||||
|
|
@ -74,6 +82,7 @@ const {
|
|||
createAgent: createAgentHandler,
|
||||
getAgent: getAgentHandler,
|
||||
duplicateAgent: duplicateAgentHandler,
|
||||
uploadAgentAvatar: uploadAgentAvatarHandler,
|
||||
revertAgentVersion: revertAgentVersionHandler,
|
||||
updateAgent: updateAgentHandler,
|
||||
getListAgents: getListAgentsHandler,
|
||||
|
|
@ -86,12 +95,17 @@ const {
|
|||
} = require('~/server/services/PermissionService');
|
||||
|
||||
const { refreshS3Url } = require('@librechat/api');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
|
||||
|
||||
/**
|
||||
* @type {import('mongoose').Model<import('@librechat/data-schemas').IAgent>}
|
||||
*/
|
||||
let Agent;
|
||||
|
||||
const encryptStoredSecret = async (value) => encryptV2(encodeURIComponent(value));
|
||||
const decryptStoredSecret = async (value) => decodeURIComponent(await decryptV2(value));
|
||||
|
||||
describe('Agent Controllers - Mass Assignment Protection', () => {
|
||||
let mongoServer;
|
||||
let mockReq;
|
||||
|
|
@ -203,12 +217,12 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
|||
});
|
||||
|
||||
const agentInDb = await Agent.findOne({ id: createdAgent.id }).lean();
|
||||
expect(agentInDb.langfuse).toEqual({
|
||||
enabled: true,
|
||||
publicKey: 'pk-test',
|
||||
secretKey: 'sk-test',
|
||||
baseUrl: 'https://cloud.langfuse.com',
|
||||
});
|
||||
expect(agentInDb.langfuse.enabled).toBe(true);
|
||||
expect(agentInDb.langfuse.publicKey).toBe('pk-test');
|
||||
expect(agentInDb.langfuse.secretKey).not.toBe('sk-test');
|
||||
expect(agentInDb.langfuse.secretKey).toContain(':');
|
||||
expect(await decryptStoredSecret(agentInDb.langfuse.secretKey)).toBe('sk-test');
|
||||
expect(agentInDb.langfuse.baseUrl).toBe('https://cloud.langfuse.com');
|
||||
});
|
||||
|
||||
test('should reject creation with unauthorized fields (mass assignment protection)', async () => {
|
||||
|
|
@ -785,13 +799,14 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
|||
});
|
||||
|
||||
test('should preserve existing Langfuse secret when update sends an empty secret', async () => {
|
||||
const encryptedOriginal = await encryptStoredSecret('sk-original');
|
||||
await Agent.updateOne(
|
||||
{ id: existingAgentId },
|
||||
{
|
||||
langfuse: {
|
||||
enabled: true,
|
||||
publicKey: 'pk-original',
|
||||
secretKey: 'sk-original',
|
||||
secretKey: encryptedOriginal,
|
||||
baseUrl: 'https://cloud.langfuse.com',
|
||||
},
|
||||
},
|
||||
|
|
@ -819,22 +834,22 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
|||
});
|
||||
|
||||
const agentInDb = await Agent.findOne({ id: existingAgentId }).lean();
|
||||
expect(agentInDb.langfuse).toEqual({
|
||||
enabled: true,
|
||||
publicKey: 'pk-updated',
|
||||
secretKey: 'sk-original',
|
||||
baseUrl: 'https://us.cloud.langfuse.com',
|
||||
});
|
||||
expect(agentInDb.langfuse.enabled).toBe(true);
|
||||
expect(agentInDb.langfuse.publicKey).toBe('pk-updated');
|
||||
expect(agentInDb.langfuse.secretKey).toBe(encryptedOriginal);
|
||||
expect(await decryptStoredSecret(agentInDb.langfuse.secretKey)).toBe('sk-original');
|
||||
expect(agentInDb.langfuse.baseUrl).toBe('https://us.cloud.langfuse.com');
|
||||
});
|
||||
|
||||
test('should update Langfuse secret explicitly while redacting it in update response', async () => {
|
||||
const encryptedOriginal = await encryptStoredSecret('sk-original');
|
||||
await Agent.updateOne(
|
||||
{ id: existingAgentId },
|
||||
{
|
||||
langfuse: {
|
||||
enabled: true,
|
||||
publicKey: 'pk-original',
|
||||
secretKey: 'sk-original',
|
||||
secretKey: encryptedOriginal,
|
||||
baseUrl: 'https://cloud.langfuse.com',
|
||||
},
|
||||
},
|
||||
|
|
@ -857,7 +872,50 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
|||
expect(updatedAgent.langfuse.secretKey).toBe('');
|
||||
|
||||
const agentInDb = await Agent.findOne({ id: existingAgentId }).lean();
|
||||
expect(agentInDb.langfuse.secretKey).toBe('sk-updated');
|
||||
expect(agentInDb.langfuse.secretKey).not.toBe('sk-updated');
|
||||
expect(agentInDb.langfuse.secretKey).not.toBe(encryptedOriginal);
|
||||
expect(await decryptStoredSecret(agentInDb.langfuse.secretKey)).toBe('sk-updated');
|
||||
});
|
||||
|
||||
test('uploadAgentAvatarHandler should redact Langfuse secret in response', async () => {
|
||||
await Agent.updateOne(
|
||||
{ id: existingAgentId },
|
||||
{
|
||||
langfuse: {
|
||||
enabled: true,
|
||||
publicKey: 'pk-avatar',
|
||||
secretKey: 'sk-avatar',
|
||||
baseUrl: 'https://cloud.langfuse.com',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const readFileSpy = jest.spyOn(fs, 'readFile').mockResolvedValue(Buffer.from('avatar'));
|
||||
const unlinkSpy = jest.spyOn(fs, 'unlink').mockResolvedValue();
|
||||
getStrategyFunctions.mockReturnValue({
|
||||
processAvatar: jest.fn().mockResolvedValue('avatars/new-avatar.png'),
|
||||
});
|
||||
resizeAvatar.mockResolvedValue(Buffer.from('resized-avatar'));
|
||||
|
||||
mockReq.user.id = existingAgentAuthorId.toString();
|
||||
mockReq.user.tenantId = 'tenant-1';
|
||||
mockReq.params.agent_id = existingAgentId;
|
||||
mockReq.config = {};
|
||||
mockReq.file = {
|
||||
path: '/tmp/avatar.png',
|
||||
};
|
||||
|
||||
await uploadAgentAvatarHandler(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.status).toHaveBeenCalledWith(201);
|
||||
const updatedAgent = mockRes.json.mock.calls[0][0];
|
||||
expect(updatedAgent.langfuse.secretKey).toBe('');
|
||||
|
||||
const agentInDb = await Agent.findOne({ id: existingAgentId }).lean();
|
||||
expect(agentInDb.langfuse.secretKey).toBe('sk-avatar');
|
||||
|
||||
readFileSpy.mockRestore();
|
||||
unlinkSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('should return 404 for non-existent agent', async () => {
|
||||
|
|
@ -1159,6 +1217,44 @@ describe('Agent Controllers - Mass Assignment Protection', () => {
|
|||
const agentInDb = await Agent.findOne({ id: agent.id }).lean();
|
||||
expect(agentInDb.tool_resources.file_search.file_ids).toEqual([ownedFileId]);
|
||||
});
|
||||
|
||||
test('revertAgentVersionHandler should redact Langfuse secret in response', async () => {
|
||||
const agentAuthorId = new mongoose.Types.ObjectId();
|
||||
const agent = await Agent.create({
|
||||
id: `agent_${uuidv4()}`,
|
||||
name: 'Current Agent',
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
author: agentAuthorId,
|
||||
versions: [
|
||||
{
|
||||
name: 'Historical Agent',
|
||||
provider: 'openai',
|
||||
model: 'gpt-4',
|
||||
langfuse: {
|
||||
enabled: true,
|
||||
publicKey: 'pk-version',
|
||||
secretKey: 'sk-version',
|
||||
baseUrl: 'https://cloud.langfuse.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
mockReq.user.id = agentAuthorId.toString();
|
||||
mockReq.params.id = agent.id;
|
||||
mockReq.body = { version_index: 0 };
|
||||
|
||||
await revertAgentVersionHandler(mockReq, mockRes);
|
||||
|
||||
expect(mockRes.json).toHaveBeenCalled();
|
||||
const updatedAgent = mockRes.json.mock.calls[0][0];
|
||||
expect(updatedAgent.langfuse.secretKey).toBe('');
|
||||
|
||||
const agentInDb = await Agent.findOne({ id: agent.id }).lean();
|
||||
expect(agentInDb.langfuse.secretKey).not.toBe('sk-version');
|
||||
expect(await decryptStoredSecret(agentInDb.langfuse.secretKey)).toBe('sk-version');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mass Assignment Attack Scenarios', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Providers } from '@librechat/agents';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import { logger, decryptV2 } from '@librechat/data-schemas';
|
||||
import { ToolMessage, AIMessage, HumanMessage } from '@librechat/agents/langchain/messages';
|
||||
|
||||
import {
|
||||
|
|
@ -8,6 +8,18 @@ import {
|
|||
extractDiscoveredToolsFromHistory,
|
||||
} from './run';
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => {
|
||||
const actual = jest.requireActual('@librechat/data-schemas');
|
||||
return {
|
||||
...actual,
|
||||
decryptV2: jest.fn(async (value: string) =>
|
||||
value === '0123456789abcdef0123456789abcdef:736b2d6167656e74'
|
||||
? encodeURIComponent('sk-agent')
|
||||
: value,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
type LangfuseRunAgent = Parameters<typeof resolveEffectiveLangfuseConfig>[0];
|
||||
type LangfuseAppConfig = NonNullable<Parameters<typeof resolveEffectiveLangfuseConfig>[1]>;
|
||||
|
||||
|
|
@ -188,14 +200,14 @@ describe('resolveEffectiveLangfuseConfig', () => {
|
|||
delete process.env.LANGFUSE_TEST_MISSING_KEY;
|
||||
});
|
||||
|
||||
it('returns undefined when no tenant or agent config is supplied', () => {
|
||||
it('returns undefined when no tenant or agent config is supplied', async () => {
|
||||
expect(
|
||||
resolveEffectiveLangfuseConfig(createLangfuseAgent(), createLangfuseAppConfig()),
|
||||
await resolveEffectiveLangfuseConfig(createLangfuseAgent(), createLangfuseAppConfig()),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses tenant defaults and resolves env var refs', () => {
|
||||
const result = resolveEffectiveLangfuseConfig(
|
||||
it('uses tenant defaults and resolves env var refs', async () => {
|
||||
const result = await resolveEffectiveLangfuseConfig(
|
||||
createLangfuseAgent(),
|
||||
createLangfuseAppConfig({
|
||||
enabled: true,
|
||||
|
|
@ -213,8 +225,8 @@ describe('resolveEffectiveLangfuseConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('overlays non-empty agent fields on tenant defaults', () => {
|
||||
const result = resolveEffectiveLangfuseConfig(
|
||||
it('overlays non-empty agent fields on tenant defaults', async () => {
|
||||
const result = await resolveEffectiveLangfuseConfig(
|
||||
createLangfuseAgent({
|
||||
enabled: true,
|
||||
publicKey: 'pk-agent',
|
||||
|
|
@ -235,8 +247,8 @@ describe('resolveEffectiveLangfuseConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('enables tracing with valid keys and no base URL', () => {
|
||||
const result = resolveEffectiveLangfuseConfig(
|
||||
it('enables tracing with valid keys and no base URL', async () => {
|
||||
const result = await resolveEffectiveLangfuseConfig(
|
||||
createLangfuseAgent(),
|
||||
createLangfuseAppConfig({
|
||||
enabled: true,
|
||||
|
|
@ -252,8 +264,31 @@ describe('resolveEffectiveLangfuseConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('lets an agent explicitly disable tracing over enabled tenant defaults', () => {
|
||||
const result = resolveEffectiveLangfuseConfig(
|
||||
it('decrypts encrypted agent secret keys before resolving the effective config', async () => {
|
||||
const encryptedSecret = '0123456789abcdef0123456789abcdef:736b2d6167656e74';
|
||||
const result = await resolveEffectiveLangfuseConfig(
|
||||
createLangfuseAgent({
|
||||
enabled: true,
|
||||
publicKey: 'pk-agent',
|
||||
secretKey: encryptedSecret,
|
||||
}),
|
||||
createLangfuseAppConfig({
|
||||
enabled: true,
|
||||
publicKey: 'pk-tenant',
|
||||
secretKey: 'sk-tenant',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(decryptV2).toHaveBeenCalledWith(encryptedSecret);
|
||||
expect(result).toEqual({
|
||||
enabled: true,
|
||||
publicKey: 'pk-agent',
|
||||
secretKey: 'sk-agent',
|
||||
});
|
||||
});
|
||||
|
||||
it('lets an agent explicitly disable tracing over enabled tenant defaults', async () => {
|
||||
const result = await resolveEffectiveLangfuseConfig(
|
||||
createLangfuseAgent({
|
||||
enabled: false,
|
||||
}),
|
||||
|
|
@ -268,8 +303,8 @@ describe('resolveEffectiveLangfuseConfig', () => {
|
|||
expect(result).toEqual({ enabled: false });
|
||||
});
|
||||
|
||||
it('disables tracing and warns when credentials are unresolved', () => {
|
||||
const result = resolveEffectiveLangfuseConfig(
|
||||
it('disables tracing and warns when credentials are unresolved', async () => {
|
||||
const result = await resolveEffectiveLangfuseConfig(
|
||||
createLangfuseAgent(),
|
||||
createLangfuseAppConfig({
|
||||
enabled: true,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import { logger, decryptV2 } from '@librechat/data-schemas';
|
||||
import { Run, Providers, Constants } from '@librechat/agents';
|
||||
import {
|
||||
KnownEndpoints,
|
||||
|
|
@ -278,6 +278,7 @@ function isNonEmptyString(value: unknown): value is string {
|
|||
}
|
||||
|
||||
const UNRESOLVED_ENV_VAR_PLACEHOLDER = /\$\{[^}]+\}/;
|
||||
const ENCRYPTED_V2_VALUE = /^[a-f0-9]{32}:[a-f0-9]+$/i;
|
||||
|
||||
function hasUnresolvedPlaceholder(value: string): boolean {
|
||||
return UNRESOLVED_ENV_VAR_PLACEHOLDER.test(value);
|
||||
|
|
@ -298,7 +299,11 @@ type EffectiveLangfuseConfig =
|
|||
enabled: false;
|
||||
};
|
||||
|
||||
function resolveLangfuseValue(agentValue?: string, tenantValue?: string): string | undefined {
|
||||
async function resolveLangfuseValue(
|
||||
agentValue?: string,
|
||||
tenantValue?: string,
|
||||
options: { encrypted?: boolean; agentId?: string } = {},
|
||||
): Promise<string | undefined> {
|
||||
const rawValue = isNonEmptyString(agentValue)
|
||||
? agentValue
|
||||
: isNonEmptyString(tenantValue)
|
||||
|
|
@ -308,7 +313,21 @@ function resolveLangfuseValue(agentValue?: string, tenantValue?: string): string
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const resolved = extractEnvVariable(rawValue);
|
||||
let value = rawValue;
|
||||
if (options.encrypted === true && ENCRYPTED_V2_VALUE.test(value)) {
|
||||
try {
|
||||
value = decodeURIComponent(await decryptV2(value));
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[createRun] Langfuse tracing disabled for agent ${
|
||||
options.agentId ?? 'unknown'
|
||||
}; failed to decrypt secretKey: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const resolved = extractEnvVariable(value);
|
||||
if (!isNonEmptyString(resolved) || hasUnresolvedPlaceholder(resolved)) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -316,10 +335,10 @@ function resolveLangfuseValue(agentValue?: string, tenantValue?: string): string
|
|||
return resolved;
|
||||
}
|
||||
|
||||
export function resolveEffectiveLangfuseConfig(
|
||||
export async function resolveEffectiveLangfuseConfig(
|
||||
agent: RunAgent,
|
||||
appConfig?: AppConfig,
|
||||
): EffectiveLangfuseConfig | undefined {
|
||||
): Promise<EffectiveLangfuseConfig | undefined> {
|
||||
const tenantLangfuse = appConfig?.langfuse;
|
||||
const agentLangfuse = agent.langfuse;
|
||||
|
||||
|
|
@ -332,9 +351,12 @@ export function resolveEffectiveLangfuseConfig(
|
|||
return { enabled: false };
|
||||
}
|
||||
|
||||
const publicKey = resolveLangfuseValue(agentLangfuse?.publicKey, tenantLangfuse?.publicKey);
|
||||
const secretKey = resolveLangfuseValue(agentLangfuse?.secretKey, tenantLangfuse?.secretKey);
|
||||
const baseUrl = resolveLangfuseValue(agentLangfuse?.baseUrl, tenantLangfuse?.baseUrl);
|
||||
const publicKey = await resolveLangfuseValue(agentLangfuse?.publicKey, tenantLangfuse?.publicKey);
|
||||
const secretKey = await resolveLangfuseValue(agentLangfuse?.secretKey, tenantLangfuse?.secretKey, {
|
||||
encrypted: true,
|
||||
agentId: agent.id,
|
||||
});
|
||||
const baseUrl = await resolveLangfuseValue(agentLangfuse?.baseUrl, tenantLangfuse?.baseUrl);
|
||||
|
||||
if (!publicKey || !secretKey) {
|
||||
const missingFields = [
|
||||
|
|
@ -697,14 +719,14 @@ function anyAgentHasCodeEnv(agents: RunAgent[]): boolean {
|
|||
* explicit child agents loaded in `agent.subagentAgentConfigs`. Returns an empty
|
||||
* array when subagents are disabled or no spawn targets are available.
|
||||
*/
|
||||
function buildSubagentConfigs(
|
||||
async function buildSubagentConfigs(
|
||||
agent: RunAgent,
|
||||
agentInput: AgentInputs,
|
||||
toInput: (child: RunAgent, opts?: { isSubagent?: boolean }) => AgentInputs,
|
||||
toInput: (child: RunAgent, opts?: { isSubagent?: boolean }) => Promise<AgentInputs>,
|
||||
state: SubagentBuildState,
|
||||
ancestors: Set<string> = new Set(),
|
||||
depth = 0,
|
||||
): SubagentConfig[] {
|
||||
): Promise<SubagentConfig[]> {
|
||||
if (!agent.subagents?.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -754,7 +776,7 @@ function buildSubagentConfigs(
|
|||
* skips both the field stamping and the registry mutation at the
|
||||
* source so children truly start fresh.
|
||||
*/
|
||||
const childInputs = toInput(child, { isSubagent: true });
|
||||
const childInputs = await toInput(child, { isSubagent: true });
|
||||
/**
|
||||
* Recursively resolve the child's own spawn targets so multi-level
|
||||
* delegation (A → B → C) works. Without this, a child whose own
|
||||
|
|
@ -763,7 +785,7 @@ function buildSubagentConfigs(
|
|||
* `subagentConfigs`, and that only runs for the outer agents in
|
||||
* `agents[]`. Cycle-safe via `nextAncestors`.
|
||||
*/
|
||||
const grandchildConfigs = buildSubagentConfigs(
|
||||
const grandchildConfigs = await buildSubagentConfigs(
|
||||
child,
|
||||
childInputs,
|
||||
toInput,
|
||||
|
|
@ -858,7 +880,10 @@ export async function createRun({
|
|||
? extractDiscoveredToolsFromHistory(messages)
|
||||
: new Set<string>();
|
||||
|
||||
const buildAgentInput = (agent: RunAgent, opts: { isSubagent?: boolean } = {}): AgentInputs => {
|
||||
const buildAgentInput = async (
|
||||
agent: RunAgent,
|
||||
opts: { isSubagent?: boolean } = {},
|
||||
): Promise<AgentInputs> => {
|
||||
const isSubagent = opts.isSubagent === true;
|
||||
const provider =
|
||||
(providerEndpointMap[
|
||||
|
|
@ -981,7 +1006,7 @@ export async function createRun({
|
|||
);
|
||||
|
||||
const reasoningKey = getReasoningKey(provider, llmConfig, agent.endpoint);
|
||||
const langfuse = resolveEffectiveLangfuseConfig(agent, appConfig);
|
||||
const langfuse = await resolveEffectiveLangfuseConfig(agent, appConfig);
|
||||
const agentInput: AgentInputs & { langfuse?: LangfuseConfig } = {
|
||||
provider,
|
||||
reasoningKey,
|
||||
|
|
@ -1015,8 +1040,8 @@ export async function createRun({
|
|||
rootAgentIds: agents.map((agent) => agent.id),
|
||||
};
|
||||
for (const agent of agents) {
|
||||
const agentInput = buildAgentInput(agent);
|
||||
const subagentConfigs = buildSubagentConfigs(
|
||||
const agentInput = await buildAgentInput(agent);
|
||||
const subagentConfigs = await buildSubagentConfigs(
|
||||
agent,
|
||||
agentInput,
|
||||
buildAgentInput,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue