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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Force LF line endings for shell scripts and git hooks (required for cross-platform compatibility)
.husky/* text eol=lf
*.sh text eol=lf
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
#!/bin/sh
[ -n "$CI" ] && exit 0
npx lint-staged --config ./.husky/lint-staged.config.js
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ The source code for `@librechat/agents` (major backend dependency, same team) is

## Code Style

### Naming and File Organization

- **Single-word file names** whenever possible (e.g., `permissions.ts`, `capabilities.ts`, `service.ts`).
- When multiple words are needed, prefer grouping related modules under a **single-word directory** rather than using multi-word file names (e.g., `admin/capabilities.ts` not `adminCapabilities.ts`).
- The directory already provides context — `app/service.ts` not `app/appConfigService.ts`.

### Structure and Clarity

- **Never-nesting**: early returns, flat code, minimal indentation. Break complex operations into well-named helpers.
Expand Down
1 change: 1 addition & 0 deletions api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const startServer = async () => {
/* API Endpoints */
app.use('/api/auth', routes.auth);
app.use('/api/admin', routes.adminAuth);
app.use('/api/admin/config', routes.adminConfig);
app.use('/api/actions', routes.actions);
app.use('/api/keys', routes.keys);
app.use('/api/api-keys', routes.apiKeys);
Expand Down
3 changes: 2 additions & 1 deletion api/server/middleware/config/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const { getAppConfig } = require('~/server/services/Config');
const configMiddleware = async (req, res, next) => {
try {
const userRole = req.user?.role;
req.config = await getAppConfig({ role: userRole });
const userId = req.user?.id;
req.config = await getAppConfig({ role: userRole, userId });

next();
} catch (error) {
Expand Down
36 changes: 36 additions & 0 deletions api/server/routes/admin/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const express = require('express');
const { createAdminConfigHandlers } = require('@librechat/api');
const { SystemCapabilities } = require('@librechat/data-schemas');
const {
requireCapability,
hasConfigCapability,
} = require('~/server/middleware/roles/capabilities');
const { requireJwtAuth } = require('~/server/middleware');
const { signalConfigChange } = require('~/server/services/Config/app');
const db = require('~/models');

const router = express.Router();

const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);

const handlers = createAdminConfigHandlers({
findConfigByPrincipal: db.findConfigByPrincipal,
getApplicableConfigs: db.getApplicableConfigs,
upsertConfig: db.upsertConfig,
deleteConfig: db.deleteConfig,
toggleConfigActive: db.toggleConfigActive,
hasConfigCapability,
signalConfigChange,
});

router.use(requireJwtAuth, requireAdminAccess);

router.get('/', handlers.listConfigs);
router.get('/:principalType/:principalId', handlers.getConfig);
router.put('/:principalType/:principalId', handlers.upsertConfigOverrides);
router.patch('/:principalType/:principalId/fields', handlers.patchConfigField);
router.delete('/:principalType/:principalId/fields', handlers.deleteConfigField);
router.delete('/:principalType/:principalId', handlers.deleteConfigOverrides);
router.patch('/:principalType/:principalId/active', handlers.toggleConfig);

module.exports = router;
2 changes: 2 additions & 0 deletions api/server/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const accessPermissions = require('./accessPermissions');
const assistants = require('./assistants');
const categories = require('./categories');
const adminAuth = require('./admin/auth');
const adminConfig = require('./admin/config');
const endpoints = require('./endpoints');
const staticRoute = require('./static');
const messages = require('./messages');
Expand Down Expand Up @@ -31,6 +32,7 @@ module.exports = {
mcp,
auth,
adminAuth,
adminConfig,
keys,
apiKeys,
user,
Expand Down
72 changes: 12 additions & 60 deletions api/server/services/Config/app.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
const { CacheKeys } = require('librechat-data-provider');
const { logger, AppService } = require('@librechat/data-schemas');
const { createAppConfigService } = require('@librechat/api');
const { AppService } = require('@librechat/data-schemas');
const { loadAndFormatTools } = require('~/server/services/start/tools');
const loadCustomConfig = require('./loadCustomConfig');
const { setCachedTools } = require('./getCachedTools');
const getLogStores = require('~/cache/getLogStores');
const paths = require('~/config/paths');

const BASE_CONFIG_KEY = '_BASE_';
const db = require('~/models');

const loadBaseConfig = async () => {
/** @type {TCustomConfig} */
Expand All @@ -20,65 +20,17 @@ const loadBaseConfig = async () => {
return AppService({ config, paths, systemTools });
};

/**
* Get the app configuration based on user context
* @param {Object} [options]
* @param {string} [options.role] - User role for role-based config
* @param {boolean} [options.refresh] - Force refresh the cache
* @returns {Promise<AppConfig>}
*/
async function getAppConfig(options = {}) {
const { role, refresh } = options;

const cache = getLogStores(CacheKeys.APP_CONFIG);
const cacheKey = role ? role : BASE_CONFIG_KEY;

if (!refresh) {
const cached = await cache.get(cacheKey);
if (cached) {
return cached;
}
}

let baseConfig = await cache.get(BASE_CONFIG_KEY);
if (!baseConfig) {
logger.info('[getAppConfig] App configuration not initialized. Initializing AppService...');
baseConfig = await loadBaseConfig();

if (!baseConfig) {
throw new Error('Failed to initialize app configuration through AppService.');
}

if (baseConfig.availableTools) {
await setCachedTools(baseConfig.availableTools);
}

await cache.set(BASE_CONFIG_KEY, baseConfig);
}

// For now, return the base config
// In the future, this is where we'll apply role-based modifications
if (role) {
// TODO: Apply role-based config modifications
// const roleConfig = await applyRoleBasedConfig(baseConfig, role);
// await cache.set(cacheKey, roleConfig);
// return roleConfig;
}

return baseConfig;
}

/**
* Clear the app configuration cache
* @returns {Promise<boolean>}
*/
async function clearAppConfigCache() {
const cache = getLogStores(CacheKeys.CONFIG_STORE);
const cacheKey = CacheKeys.APP_CONFIG;
return await cache.delete(cacheKey);
}
const { getAppConfig, signalConfigChange, clearAppConfigCache } = createAppConfigService({
loadBaseConfig,
setCachedTools,
getCache: getLogStores,
cacheKeys: CacheKeys,
getApplicableConfigs: db.getApplicableConfigs,
getUserPrincipals: db.getUserPrincipals,
});

module.exports = {
getAppConfig,
signalConfigChange,
clearAppConfigCache,
};
Loading
Loading