mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-29 12:22:40 +00:00
🧳 feat: Register Principal-Owned Code Environments (#15365)
* feat: add principal-owned code environments * fix: address code environment CI coverage * fix: expose principal code environments in endpoint config * fix: reject missing code environment bodies * fix: harden code environment lifecycle * fix: revalidate principal code environments * fix: synchronize code environment authorization * fix: bind code environments to current principals * fix: fail closed on code ACL cache errors * fix: prevent code environment override shadowing * fix: preserve principal environment defaults * fix: suppress revoked code environment aliases * fix: fail closed on environment augmentation * fix: narrow code environment defaults * fix: isolate code environment fallback * style: sort code config imports
This commit is contained in:
parent
0389a92452
commit
fcae1025c0
48 changed files with 2178 additions and 35 deletions
|
|
@ -20,6 +20,7 @@ const {
|
|||
searchEntraIdPrincipals,
|
||||
} = require('~/server/services/GraphApiService');
|
||||
const db = require('~/models');
|
||||
const { invalidateCodeEnvironmentConfigCache } = require('~/server/services/Config');
|
||||
|
||||
const matchesCurrentTenant = (principal, tenantId) => {
|
||||
if (!tenantId || tenantId === SYSTEM_TENANT_ID) {
|
||||
|
|
@ -157,6 +158,14 @@ const updateResourcePermissions = async (req, res) => {
|
|||
grantedBy: userId,
|
||||
});
|
||||
|
||||
if (resourceType === ResourceType.CODE_ENVIRONMENT) {
|
||||
await invalidateCodeEnvironmentConfigCache(req.user.tenantId).catch((error) => {
|
||||
// Cached environment metadata is authorization-filtered against the live ACL on every
|
||||
// read, so a failed revision write may delay a grant but cannot preserve a revocation.
|
||||
logger.error('[PermissionsController] code environment cache invalidation failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
const isAgentResource =
|
||||
resourceType === ResourceType.AGENT || resourceType === ResourceType.REMOTE_AGENT;
|
||||
const revokedUserIds = results.revoked
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const {
|
|||
cancelAgentTriggerUserPurge,
|
||||
purgeAgentTriggerDeliveriesForUser,
|
||||
} = require('~/server/services/Agents/triggers');
|
||||
const { getAppConfig } = require('~/server/services/Config');
|
||||
const { getAppConfig, invalidateCodeEnvironmentConfigCache } = require('~/server/services/Config');
|
||||
const { randomUUID } = require('node:crypto');
|
||||
const {
|
||||
quiesceUserSchedules,
|
||||
|
|
@ -460,6 +460,10 @@ const deleteUserController = async (req, res) => {
|
|||
await db.deleteAllUserMemories(user.id);
|
||||
await db.deleteUserPrompts(user.id);
|
||||
await db.deleteUserSkills(user.id);
|
||||
await db.deleteUserCodeEnvironments(user.id);
|
||||
await invalidateCodeEnvironmentConfigCache(user.tenantId).catch((error) => {
|
||||
logger.error('[deleteUserController] code environment cache invalidation failed:', error);
|
||||
});
|
||||
await deleteUserMcpServers(user.id);
|
||||
await db.deleteActions({ user: user.id });
|
||||
await db.deleteTokens({ userId: user.id });
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const mockCancelAndDrainSubagentThreads = jest.fn().mockResolvedValue(undefined)
|
|||
const mockQuiesceUserSchedules = jest.fn().mockResolvedValue(true);
|
||||
const mockRestoreUserSchedules = jest.fn().mockResolvedValue(undefined);
|
||||
const mockGetWebSearchInstallEntries = jest.fn();
|
||||
const mockInvalidateCodeEnvironmentConfigCache = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => {
|
||||
const actual = jest.requireActual('@librechat/data-schemas');
|
||||
|
|
@ -43,6 +44,7 @@ jest.mock('~/models', () => {
|
|||
cancelAgentTriggerUserDeletion: jest.fn().mockResolvedValue(true),
|
||||
deleteUserPrompts: jest.fn().mockResolvedValue(undefined),
|
||||
deleteUserSkills: jest.fn().mockResolvedValue(undefined),
|
||||
deleteUserCodeEnvironments: jest.fn().mockResolvedValue(undefined),
|
||||
deleteMessages: jest.fn().mockResolvedValue(undefined),
|
||||
deleteBalances: jest.fn().mockResolvedValue(undefined),
|
||||
deleteActions: jest.fn().mockResolvedValue(undefined),
|
||||
|
|
@ -119,6 +121,8 @@ jest.mock('~/server/services/Config', () => ({
|
|||
getMCPManager: jest.fn(),
|
||||
getFlowStateManager: jest.fn(),
|
||||
getMCPServersRegistry: jest.fn(),
|
||||
invalidateCodeEnvironmentConfigCache: (...args) =>
|
||||
mockInvalidateCodeEnvironmentConfigCache(...args),
|
||||
}));
|
||||
|
||||
jest.mock('~/cache', () => ({
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@ jest.mock('@librechat/api', () => ({
|
|||
backfillRemoteAgentPermissions: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockInvalidateCodeEnvironmentConfigCache = jest.fn().mockResolvedValue(undefined);
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
invalidateCodeEnvironmentConfigCache: (...args) =>
|
||||
mockInvalidateCodeEnvironmentConfigCache(...args),
|
||||
}));
|
||||
|
||||
const mockBulkUpdateResourcePermissions = jest.fn();
|
||||
|
||||
jest.mock('~/server/services/PermissionService', () => ({
|
||||
|
|
@ -380,6 +386,49 @@ describe('PermissionsController', () => {
|
|||
expect(mockRemoveAgentFromUserFavorites).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('invalidates shared environment configuration after code environment ACL changes', async () => {
|
||||
const req = createMockReq({
|
||||
params: { resourceType: ResourceType.CODE_ENVIRONMENT, resourceId: agentObjectId },
|
||||
body: {
|
||||
updated: [{ type: PrincipalType.USER, id: revokedUserId }],
|
||||
removed: [],
|
||||
public: false,
|
||||
},
|
||||
user: { id: 'user-1', role: 'USER', tenantId: 'tenant-a' },
|
||||
});
|
||||
const res = createMockRes();
|
||||
|
||||
await updateResourcePermissions(req, res);
|
||||
|
||||
expect(mockInvalidateCodeEnvironmentConfigCache).toHaveBeenCalledWith('tenant-a');
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
it('keeps the committed ACL response successful when cache invalidation fails', async () => {
|
||||
mockInvalidateCodeEnvironmentConfigCache.mockRejectedValueOnce(
|
||||
new Error('redis unavailable'),
|
||||
);
|
||||
const req = createMockReq({
|
||||
params: { resourceType: ResourceType.CODE_ENVIRONMENT, resourceId: agentObjectId },
|
||||
body: {
|
||||
updated: [],
|
||||
removed: [{ type: PrincipalType.USER, id: revokedUserId }],
|
||||
public: false,
|
||||
},
|
||||
user: { id: 'user-1', role: 'USER', tenantId: 'tenant-a' },
|
||||
});
|
||||
const res = createMockRes();
|
||||
|
||||
await updateResourcePermissions(req, res);
|
||||
|
||||
expect(mockInvalidateCodeEnvironmentConfigCache).toHaveBeenCalledWith('tenant-a');
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
'[PermissionsController] code environment cache invalidation failed:',
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
it('handles agent not found gracefully', async () => {
|
||||
mockRemoveAgentFromUserFavorites.mockResolvedValue(undefined);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ const mockDeleteToolCalls = jest.fn();
|
|||
const mockDeleteUserAgents = jest.fn();
|
||||
const mockDeleteUserPrompts = jest.fn();
|
||||
const mockDeleteUserSkills = jest.fn();
|
||||
const mockDeleteUserCodeEnvironments = jest.fn();
|
||||
const mockInvalidateCodeEnvironmentConfigCache = jest.fn();
|
||||
const mockGetCleanupBlockingJobIdsForUser = jest.fn();
|
||||
const mockAbortJob = jest.fn();
|
||||
const mockDrainAgentTriggerDeliveriesForUser = jest.fn();
|
||||
|
|
@ -77,6 +79,7 @@ jest.mock('~/models', () => ({
|
|||
deleteUserAgents: (...args) => mockDeleteUserAgents(...args),
|
||||
deleteUserPrompts: (...args) => mockDeleteUserPrompts(...args),
|
||||
deleteUserSkills: (...args) => mockDeleteUserSkills(...args),
|
||||
deleteUserCodeEnvironments: (...args) => mockDeleteUserCodeEnvironments(...args),
|
||||
deleteTransactions: jest.fn(),
|
||||
deleteBalances: jest.fn(),
|
||||
deleteAllAgentApiKeys: jest.fn(),
|
||||
|
|
@ -136,6 +139,8 @@ jest.mock('~/server/services/Schedules', () => ({
|
|||
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
getAppConfig: jest.fn(),
|
||||
invalidateCodeEnvironmentConfigCache: (...args) =>
|
||||
mockInvalidateCodeEnvironmentConfigCache(...args),
|
||||
}));
|
||||
|
||||
jest.mock('~/cache', () => ({
|
||||
|
|
@ -169,6 +174,7 @@ function stubDeletionMocks() {
|
|||
mockDeleteUserAgents.mockResolvedValue();
|
||||
mockDeleteUserPrompts.mockResolvedValue();
|
||||
mockDeleteUserSkills.mockResolvedValue(0);
|
||||
mockInvalidateCodeEnvironmentConfigCache.mockResolvedValue(undefined);
|
||||
mockGetCleanupBlockingJobIdsForUser.mockResolvedValue([]);
|
||||
mockAbortJob.mockResolvedValue({ success: true });
|
||||
mockDrainAgentTriggerDeliveriesForUser.mockResolvedValue();
|
||||
|
|
@ -201,6 +207,7 @@ describe('deleteUserController - 2FA enforcement', () => {
|
|||
expect(mockDeleteUserAgents).toHaveBeenCalledWith('user1');
|
||||
expect(mockDeleteUserPrompts).toHaveBeenCalledWith('user1');
|
||||
expect(mockDeleteUserSkills).toHaveBeenCalledWith('user1');
|
||||
expect(mockInvalidateCodeEnvironmentConfigCache).toHaveBeenCalledWith(undefined);
|
||||
expect(mockVerifyOTPOrBackupCode).not.toHaveBeenCalled();
|
||||
expect(mockBeginAgentTriggerUserDeletion.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mockPrepareAgentTriggerUserPurge.mock.invocationCallOrder[0],
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const { ResourceType } = require('librechat-data-provider');
|
|||
*/
|
||||
const HANDLED_RESOURCE_TYPES = {
|
||||
[ResourceType.AGENT]: 'deleteUserAgents',
|
||||
[ResourceType.CODE_ENVIRONMENT]: 'deleteUserCodeEnvironments',
|
||||
[ResourceType.REMOTE_AGENT]: 'deleteUserAgents',
|
||||
[ResourceType.PROMPTGROUP]: 'deleteUserPrompts',
|
||||
[ResourceType.MCPSERVER]: 'deleteUserMcpServers',
|
||||
|
|
|
|||
|
|
@ -521,6 +521,7 @@ if (cluster.isMaster) {
|
|||
app.use('/api/admin', routes.adminAuth);
|
||||
app.use('/api/admin/skills', routes.adminSkills);
|
||||
app.use('/api/admin/code-environments', routes.adminCodeEnvironments);
|
||||
app.use('/api/code-environments', routes.codeEnvironments);
|
||||
app.use('/api/actions', routes.actions);
|
||||
app.use('/api/keys', routes.keys);
|
||||
app.use('/api/api-keys', routes.apiKeys);
|
||||
|
|
|
|||
|
|
@ -368,6 +368,7 @@ const startServer = async () => {
|
|||
app.use('/api/admin', routes.adminAuth);
|
||||
app.use('/api/admin/config', routes.adminConfig);
|
||||
app.use('/api/admin/code-environments', routes.adminCodeEnvironments);
|
||||
app.use('/api/code-environments', routes.codeEnvironments);
|
||||
app.use('/api/admin/langfuse', routes.adminLangfuse);
|
||||
app.use('/api/admin/grants', routes.adminGrants);
|
||||
app.use('/api/admin/groups', routes.adminGroups);
|
||||
|
|
|
|||
|
|
@ -88,6 +88,12 @@ const checkResourcePermissionAccess = (requiredPermission) => (req, res, next) =
|
|||
resourceIdParam: 'resourceId',
|
||||
idResolver: getSkillById,
|
||||
});
|
||||
} else if (resourceType === ResourceType.CODE_ENVIRONMENT) {
|
||||
middleware = canAccessResource({
|
||||
resourceType: ResourceType.CODE_ENVIRONMENT,
|
||||
requiredPermission,
|
||||
resourceIdParam: 'resourceId',
|
||||
});
|
||||
} else if (resourceType === ResourceType.SHARED_LINK) {
|
||||
middleware = canAccessResource({
|
||||
resourceType: ResourceType.SHARED_LINK,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const {
|
|||
purgeAgentTriggerDeliveriesForUser,
|
||||
} = require('~/server/services/Agents/triggers');
|
||||
const db = require('~/models');
|
||||
const { invalidateCodeEnvironmentConfigCache } = require('~/server/services/Config');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
|
@ -27,6 +28,8 @@ const handlers = createAdminUsersHandlers({
|
|||
cancelAgentTriggerUserPurge,
|
||||
purgeAgentTriggerDeliveriesForUser,
|
||||
deleteUserById: db.deleteUserById,
|
||||
deleteUserCodeEnvironments: db.deleteUserCodeEnvironments,
|
||||
invalidateCodeEnvironmentConfigCache,
|
||||
deleteConfig: db.deleteConfig,
|
||||
deleteAclEntries: db.deleteAclEntries,
|
||||
});
|
||||
|
|
|
|||
27
api/server/routes/code-environments.js
Normal file
27
api/server/routes/code-environments.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
const express = require('express');
|
||||
const { createCodeEnvironmentHttpHandlers } = require('@librechat/api');
|
||||
const { SystemCapabilities } = require('@librechat/data-schemas');
|
||||
const { requireCapability } = require('~/server/middleware/roles/capabilities');
|
||||
const { getAppConfig, getCodeEnvironmentRegistry } = require('~/server/services/Config');
|
||||
const { requireJwtAuth } = require('~/server/middleware');
|
||||
|
||||
const router = express.Router();
|
||||
let handlers;
|
||||
function getHandlers() {
|
||||
if (handlers == null) {
|
||||
handlers = createCodeEnvironmentHttpHandlers({
|
||||
getAppConfig,
|
||||
registry: getCodeEnvironmentRegistry(),
|
||||
});
|
||||
}
|
||||
return handlers;
|
||||
}
|
||||
const requireCodeEnvironmentManage = requireCapability(SystemCapabilities.MANAGE_CODE_ENVIRONMENTS);
|
||||
|
||||
router.use(requireJwtAuth);
|
||||
router.get('/', (req, res, next) => getHandlers().list(req, res, next));
|
||||
router.post('/', requireCodeEnvironmentManage, (req, res, next) =>
|
||||
getHandlers().register(req, res, next),
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
77
api/server/routes/code-environments.test.js
Normal file
77
api/server/routes/code-environments.test.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
const express = require('express');
|
||||
const request = require('supertest');
|
||||
|
||||
const middlewareCalls = [];
|
||||
const mockRequireJwtAuth = jest.fn((req, _res, next) => {
|
||||
middlewareCalls.push('jwt');
|
||||
req.user = { id: '68b2f0c498f24c1e78fa0001', role: 'USER' };
|
||||
next();
|
||||
});
|
||||
const mockRequireCapability = jest.fn((capability) => (req, _res, next) => {
|
||||
middlewareCalls.push(capability);
|
||||
next();
|
||||
});
|
||||
const mockRegistry = {};
|
||||
const mockGetCodeEnvironmentRegistry = jest.fn(() => mockRegistry);
|
||||
const mockHandlers = {
|
||||
list: jest.fn((_req, res) => res.status(200).json({ environments: [] })),
|
||||
register: jest.fn((_req, res) => res.status(201).json({ environment: { id: 'code-1' } })),
|
||||
};
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => ({
|
||||
SystemCapabilities: { MANAGE_CODE_ENVIRONMENTS: 'manage:code_environments' },
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/api', () => ({
|
||||
createCodeEnvironmentRegistry: jest.fn(() => mockRegistry),
|
||||
createCodeEnvironmentHttpHandlers: jest.fn(() => mockHandlers),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/middleware/roles/capabilities', () => ({
|
||||
requireCapability: mockRequireCapability,
|
||||
}));
|
||||
|
||||
jest.mock('~/server/middleware', () => ({ requireJwtAuth: mockRequireJwtAuth }));
|
||||
jest.mock('~/server/services/Config', () => ({
|
||||
getAppConfig: jest.fn(),
|
||||
getCodeEnvironmentRegistry: mockGetCodeEnvironmentRegistry,
|
||||
}));
|
||||
|
||||
function createApp() {
|
||||
delete require.cache[require.resolve('./code-environments')];
|
||||
const router = require('./code-environments');
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/code-environments', router);
|
||||
return app;
|
||||
}
|
||||
|
||||
describe('code environment routes', () => {
|
||||
beforeEach(() => {
|
||||
middlewareCalls.length = 0;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('defers registry initialization until the route is requested', () => {
|
||||
createApp();
|
||||
|
||||
expect(mockGetCodeEnvironmentRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows authenticated discovery without the management capability', async () => {
|
||||
await request(createApp()).get('/api/code-environments').expect(200, { environments: [] });
|
||||
|
||||
expect(middlewareCalls).toEqual(['jwt']);
|
||||
expect(mockHandlers.list).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('requires the management capability before registration', async () => {
|
||||
await request(createApp())
|
||||
.post('/api/code-environments')
|
||||
.send({ name: 'Personal VM' })
|
||||
.expect(201);
|
||||
|
||||
expect(middlewareCalls).toEqual(['jwt', 'manage:code_environments']);
|
||||
expect(mockHandlers.register).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -6,7 +6,7 @@ const tokenConfigController = require('~/server/controllers/TokenConfigControlle
|
|||
|
||||
const router = express.Router();
|
||||
/** Auth required for role/tenant-scoped endpoint config resolution. */
|
||||
router.get('/', requireJwtAuth, endpointController);
|
||||
router.get('/', requireJwtAuth, configMiddleware, endpointController);
|
||||
router.get('/token-config', requireJwtAuth, configMiddleware, tokenConfigController);
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const categories = require('./categories');
|
|||
const adminAuth = require('./admin/auth');
|
||||
const adminConfig = require('./admin/config');
|
||||
const adminCodeEnvironments = require('./admin/code');
|
||||
const codeEnvironments = require('./code-environments');
|
||||
const adminLangfuse = require('./admin/langfuse');
|
||||
const adminGrants = require('./admin/grants');
|
||||
const adminGroups = require('./admin/groups');
|
||||
|
|
@ -49,6 +50,7 @@ module.exports = {
|
|||
adminAuth,
|
||||
adminConfig,
|
||||
adminCodeEnvironments,
|
||||
codeEnvironments,
|
||||
adminLangfuse,
|
||||
adminGrants,
|
||||
adminGroups,
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ jest.mock('~/server/services/start/tools', () => ({
|
|||
jest.mock('../loadCustomConfig', () => jest.fn().mockResolvedValue({}));
|
||||
|
||||
jest.mock('@librechat/data-schemas', () => {
|
||||
const actual = jest.requireActual('@librechat/data-schemas');
|
||||
return { ...actual, AppService: jest.fn(() => ({ availableTools: {} })) };
|
||||
return {
|
||||
AppService: jest.fn(() => ({ availableTools: {} })),
|
||||
logger: { error: jest.fn() },
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('~/models', () => ({
|
||||
|
|
@ -37,6 +39,8 @@ jest.mock('@librechat/api', () => ({
|
|||
clearOverrideCache: mockClearOverrideCache,
|
||||
})),
|
||||
clearMcpConfigCache: mockClearMcpConfigCache,
|
||||
createCodeEnvironmentRegistry: jest.fn(() => ({})),
|
||||
mergeAccessibleCodeEnvironments: jest.fn(({ appConfig }) => appConfig),
|
||||
}));
|
||||
|
||||
// ── Tests ──────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { CacheKeys } = require('librechat-data-provider');
|
||||
const { AppService, logger } = require('@librechat/data-schemas');
|
||||
const { createAppConfigService, clearMcpConfigCache } = require('@librechat/api');
|
||||
const {
|
||||
createAppConfigService,
|
||||
clearMcpConfigCache,
|
||||
createCodeEnvironmentRegistry,
|
||||
mergeAccessibleCodeEnvironments,
|
||||
cacheConfig,
|
||||
standardCache,
|
||||
} = require('@librechat/api');
|
||||
const { setCachedTools, invalidateCachedTools } = require('./getCachedTools');
|
||||
const { loadAndFormatTools } = require('~/server/services/start/tools');
|
||||
const loadCustomConfig = require('./loadCustomConfig');
|
||||
|
|
@ -8,6 +16,23 @@ const getLogStores = require('~/cache/getLogStores');
|
|||
const paths = require('~/config/paths');
|
||||
const db = require('~/models');
|
||||
|
||||
let codeEnvironmentRegistry;
|
||||
|
||||
function getCodeEnvironmentRegistry() {
|
||||
if (codeEnvironmentRegistry == null) {
|
||||
codeEnvironmentRegistry = createCodeEnvironmentRegistry(mongoose, {
|
||||
configurationCache: cacheConfig.USE_REDIS
|
||||
? standardCache('CODE_ENVIRONMENT_CONFIG')
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
return codeEnvironmentRegistry;
|
||||
}
|
||||
|
||||
async function invalidateCodeEnvironmentConfigCache(tenantId) {
|
||||
await getCodeEnvironmentRegistry().invalidateAccessibleConfigurations(tenantId);
|
||||
}
|
||||
|
||||
const loadBaseConfig = async () => {
|
||||
/** @type {TCustomConfig} */
|
||||
const config = (await loadCustomConfig()) ?? {};
|
||||
|
|
@ -27,6 +52,20 @@ const { getAppConfig, clearAppConfigCache, clearOverrideCache } = createAppConfi
|
|||
cacheKeys: CacheKeys,
|
||||
getApplicableConfigs: db.getApplicableConfigs,
|
||||
getUserPrincipals: db.getUserPrincipals,
|
||||
augmentConfig: ({ appConfig, baseConfig, principals, options }) => {
|
||||
if (!options.userId) return appConfig;
|
||||
return mergeAccessibleCodeEnvironments({
|
||||
appConfig,
|
||||
deploymentConfig: baseConfig,
|
||||
actor: {
|
||||
userId: options.userId,
|
||||
role: options.role ?? null,
|
||||
idOnTheSource: options.idOnTheSource ?? null,
|
||||
principals,
|
||||
},
|
||||
registry: getCodeEnvironmentRegistry(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -58,5 +97,8 @@ async function invalidateConfigCaches(tenantId) {
|
|||
module.exports = {
|
||||
getAppConfig,
|
||||
clearAppConfigCache,
|
||||
clearOverrideCache,
|
||||
invalidateConfigCaches,
|
||||
getCodeEnvironmentRegistry,
|
||||
invalidateCodeEnvironmentConfigCache,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -751,7 +751,9 @@ describe('PermissionService', () => {
|
|||
getAvailableRoles({
|
||||
resourceType: 'non_existent_type',
|
||||
}),
|
||||
).rejects.toThrow('Invalid resourceType: non_existent_type. Valid types: agent, promptGroup');
|
||||
).rejects.toThrow(
|
||||
'Invalid resourceType: non_existent_type. Valid types: agent, codeEnvironment, promptGroup, mcpServer, remoteAgent, skill, sharedLink',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue