Skip to content
Merged
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
1 change: 1 addition & 0 deletions api/server/experimental.js
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ if (cluster.isMaster) {
app.use('/api/admin/insights', routes.insights);
app.use('/api/admin', routes.adminAuth);
app.use('/api/admin/skills', routes.adminSkills);
app.use('/api/admin/code-environments', routes.adminCodeEnvironments);
app.use('/api/actions', routes.actions);
app.use('/api/keys', routes.keys);
app.use('/api/api-keys', routes.apiKeys);
Expand Down
1 change: 1 addition & 0 deletions api/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ const startServer = async () => {
app.use('/api/admin/insights', routes.insights);
app.use('/api/admin', routes.adminAuth);
app.use('/api/admin/config', routes.adminConfig);
app.use('/api/admin/code-environments', routes.adminCodeEnvironments);
app.use('/api/admin/langfuse', routes.adminLangfuse);
app.use('/api/admin/grants', routes.adminGrants);
app.use('/api/admin/groups', routes.adminGroups);
Expand Down
17 changes: 17 additions & 0 deletions api/server/routes/admin/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const express = require('express');
const { createAdminCodeEnvironmentHandlers } = require('@librechat/api');
const { SystemCapabilities } = require('@librechat/data-schemas');
const { requireCapability } = require('~/server/middleware/roles/capabilities');
const { getAppConfig } = require('~/server/services/Config');
const { requireJwtAuth } = require('~/server/middleware');

const router = express.Router();
const requireAdminAccess = requireCapability(SystemCapabilities.ACCESS_ADMIN);
const requireCodeEnvironmentManage = requireCapability(SystemCapabilities.MANAGE_CODE_ENVIRONMENTS);
const handlers = createAdminCodeEnvironmentHandlers({ getAppConfig });

router.use(requireJwtAuth, requireAdminAccess, requireCodeEnvironmentManage);
router.post('/:environmentId/pairings', handlers.createPairing);
router.post('/:environmentId/revoke', handlers.revokeWorker);
Comment thread
danny-avila marked this conversation as resolved.

module.exports = router;
74 changes: 74 additions & 0 deletions api/server/routes/admin/code.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const express = require('express');
const request = require('supertest');

const middlewareCalls = [];
const mockRequireJwtAuth = jest.fn((req, _res, next) => {
middlewareCalls.push('jwt');
req.user = { id: 'admin-1', role: 'ADMIN' };
next();
});
const mockRequireCapability = jest.fn((capability) => (req, _res, next) => {
middlewareCalls.push(capability);
next();
});
const mockHandlers = {
createPairing: jest.fn((req, res) =>
res.status(200).json({ operation: 'pair', environmentId: req.params.environmentId }),
),
revokeWorker: jest.fn((req, res) =>
res.status(200).json({ operation: 'revoke', environmentId: req.params.environmentId }),
),
};
const mockGetAppConfig = jest.fn();

jest.mock('@librechat/data-schemas', () => ({
SystemCapabilities: {
ACCESS_ADMIN: 'access:admin',
MANAGE_CODE_ENVIRONMENTS: 'manage:code_environments',
},
}));

jest.mock('@librechat/api', () => ({
createAdminCodeEnvironmentHandlers: jest.fn(() => mockHandlers),
}));

jest.mock('~/server/middleware/roles/capabilities', () => ({
requireCapability: mockRequireCapability,
}));

jest.mock('~/server/middleware', () => ({
requireJwtAuth: mockRequireJwtAuth,
}));

jest.mock('~/server/services/Config', () => ({
getAppConfig: mockGetAppConfig,
}));

function createApp() {
delete require.cache[require.resolve('./code')];
const router = require('./code');
const app = express();
app.use(express.json());
app.use('/api/admin/code-environments', router);
return app;
}

describe('admin code environment routes', () => {
beforeEach(() => {
middlewareCalls.length = 0;
jest.clearAllMocks();
});

it.each([
['pairings', 'createPairing', 'pair'],
['revoke', 'revokeWorker', 'revoke'],
])('protects and delegates the %s operation', async (path, handlerName, operation) => {
const response = await request(createApp())
.post(`/api/admin/code-environments/attached-vm/${path}`)
.expect(200);

expect(response.body).toEqual({ operation, environmentId: 'attached-vm' });
expect(middlewareCalls).toEqual(['jwt', 'access:admin', 'manage:code_environments']);
expect(mockHandlers[handlerName]).toHaveBeenCalledTimes(1);
});
});
2 changes: 2 additions & 0 deletions api/server/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const assistants = require('./assistants');
const categories = require('./categories');
const adminAuth = require('./admin/auth');
const adminConfig = require('./admin/config');
const adminCodeEnvironments = require('./admin/code');
const adminLangfuse = require('./admin/langfuse');
const adminGrants = require('./admin/grants');
const adminGroups = require('./admin/groups');
Expand Down Expand Up @@ -47,6 +48,7 @@ module.exports = {
auth,
adminAuth,
adminConfig,
adminCodeEnvironments,
adminLangfuse,
adminGrants,
adminGroups,
Expand Down
12 changes: 12 additions & 0 deletions e2e/playwright.config.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ const baseEnv = {
ASSISTANTS_API_KEY: 'e2e-mock-assistants-key',
ASSISTANTS_BASE_URL: `http://127.0.0.1:${ASSISTANTS_PORT}/v1`,
ASSISTANTS_MODELS: 'gpt-4o-mini',
...(process.env.E2E_CODE_BRIDGE_ADMIN_TOKEN
? { E2E_CODE_BRIDGE_ADMIN_TOKEN: process.env.E2E_CODE_BRIDGE_ADMIN_TOKEN }
: {}),
...vanillaOverrides,
};

Expand Down Expand Up @@ -210,6 +213,14 @@ function writeRuntimeMockConfig() {
.replace('# __E2E_DYNAMIC_MCP_STDIO_ENV__', dynamicMcpConfig.stdioEnv)
.replace('# __E2E_DYNAMIC_MCP_NETWORK_SERVERS__', dynamicMcpConfig.networkServers);
const codeBridgeURL = process.env.E2E_CODE_BRIDGE_URL;
const codeBridgePairing = process.env.E2E_CODE_BRIDGE_ADMIN_TOKEN
? [
' owner: deployment',
' pairing:',
' workerId: e2e-vm',
' tokenEnv: E2E_CODE_BRIDGE_ADMIN_TOKEN',
]
: [];
config = config.replace(
'# __E2E_CODE_BRIDGE_CONFIG__',
codeBridgeURL
Expand All @@ -223,6 +234,7 @@ function writeRuntimeMockConfig() {
' type: attached',
` baseURL: ${JSON.stringify(codeBridgeURL)}`,
' default: true',
...codeBridgePairing,
].join('\n ')
: '# __E2E_CODE_BRIDGE_CONFIG__',
);
Expand Down
22 changes: 22 additions & 0 deletions e2e/specs/mock/stateful-code-bridge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import {

const CODE_VALUE = 'librechat-bridge-persisted';

interface PairingResponse {
environmentId: string;
workerId: string;
code: string;
expiresAt: string;
}

test.describe('attached stateful code environment', () => {
test.skip(!process.env.E2E_CODE_BRIDGE_URL, 'E2E_CODE_BRIDGE_URL is required');

Expand All @@ -26,6 +33,21 @@ test.describe('attached stateful code environment', () => {

try {
const token = await getAccessToken(page);
if (process.env.E2E_CODE_BRIDGE_ADMIN_TOKEN) {
const pairing = await requestJson<PairingResponse>(page, {
path: '/api/admin/code-environments/e2e-vm/pairings',
token,
method: 'POST',
});
expect(pairing).toMatchObject({
environmentId: 'e2e-vm',
workerId: 'e2e-vm',
code: expect.stringMatching(/^[A-Za-z0-9_-]{32}$/),
expiresAt: expect.any(String),
});
expect(pairing).not.toHaveProperty('token');
expect(Number.isFinite(Date.parse(pairing.expiresAt))).toBe(true);
}
const agent = await requestJson<AgentDetail>(page, {
path: '/api/agents',
token,
Expand Down
6 changes: 6 additions & 0 deletions librechat.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,12 @@ endpoints:
# name: Engineering VM
# type: attached
# baseURL: https://code-bridge.example.com/v1
# # Enables the admin pairing API without placing the administrator
# # secret in YAML or returning its environment-variable name to users.
# owner: deployment
# pairing:
# workerId: engineering-vm
# tokenEnv: CODE_BRIDGE_ADMIN_TOKEN
# # (optional) Trusted origin for internal event delivery. By default, LibreChat
# # uses its own bound listener; override only for an internal TLS/front-door route.
# eventDriven:
Expand Down
Loading
Loading