From 12816e9cbd842b51b3e2d16c087e498ae5d11a6b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 07:29:07 -0400 Subject: [PATCH 1/8] feat: add secure code environment pairing --- api/server/experimental.js | 1 + api/server/index.js | 1 + api/server/routes/admin/code.js | 16 ++ api/server/routes/admin/code.test.js | 71 +++++++ api/server/routes/index.js | 2 + e2e/playwright.config.mock.ts | 12 ++ e2e/specs/mock/stateful-code-bridge.spec.ts | 22 +++ librechat.example.yaml | 6 + packages/api/src/admin/code.spec.ts | 140 +++++++++++++ packages/api/src/admin/code.ts | 186 ++++++++++++++++++ packages/api/src/admin/index.ts | 2 + .../src/endpoints/config/endpoints.spec.ts | 51 +++++ .../api/src/endpoints/config/endpoints.ts | 3 +- .../specs/config-schemas.spec.ts | 56 ++++++ packages/data-provider/src/config.ts | 18 ++ 15 files changed, 586 insertions(+), 1 deletion(-) create mode 100644 api/server/routes/admin/code.js create mode 100644 api/server/routes/admin/code.test.js create mode 100644 packages/api/src/admin/code.spec.ts create mode 100644 packages/api/src/admin/code.ts diff --git a/api/server/experimental.js b/api/server/experimental.js index b4083feedad..ff653bd9735 100644 --- a/api/server/experimental.js +++ b/api/server/experimental.js @@ -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); diff --git a/api/server/index.js b/api/server/index.js index 85ff068b5ed..e12f213f1e9 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -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); diff --git a/api/server/routes/admin/code.js b/api/server/routes/admin/code.js new file mode 100644 index 00000000000..4adced9ed4d --- /dev/null +++ b/api/server/routes/admin/code.js @@ -0,0 +1,16 @@ +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 handlers = createAdminCodeEnvironmentHandlers({ getAppConfig }); + +router.use(requireJwtAuth, requireAdminAccess); +router.post('/:environmentId/pairings', handlers.createPairing); +router.post('/:environmentId/revoke', handlers.revokeWorker); + +module.exports = router; diff --git a/api/server/routes/admin/code.test.js b/api/server/routes/admin/code.test.js new file mode 100644 index 00000000000..7f8a19c1815 --- /dev/null +++ b/api/server/routes/admin/code.test.js @@ -0,0 +1,71 @@ +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' }, +})); + +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']); + expect(mockHandlers[handlerName]).toHaveBeenCalledTimes(1); + }); +}); diff --git a/api/server/routes/index.js b/api/server/routes/index.js index 6b305686b6b..a22b065e7a4 100644 --- a/api/server/routes/index.js +++ b/api/server/routes/index.js @@ -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'); @@ -47,6 +48,7 @@ module.exports = { auth, adminAuth, adminConfig, + adminCodeEnvironments, adminLangfuse, adminGrants, adminGroups, diff --git a/e2e/playwright.config.mock.ts b/e2e/playwright.config.mock.ts index e01fc019259..0354cc7a697 100644 --- a/e2e/playwright.config.mock.ts +++ b/e2e/playwright.config.mock.ts @@ -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, }; @@ -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 @@ -223,6 +234,7 @@ function writeRuntimeMockConfig() { ' type: attached', ` baseURL: ${JSON.stringify(codeBridgeURL)}`, ' default: true', + ...codeBridgePairing, ].join('\n ') : '# __E2E_CODE_BRIDGE_CONFIG__', ); diff --git a/e2e/specs/mock/stateful-code-bridge.spec.ts b/e2e/specs/mock/stateful-code-bridge.spec.ts index 3ca71beaae8..f3b0bcd556e 100644 --- a/e2e/specs/mock/stateful-code-bridge.spec.ts +++ b/e2e/specs/mock/stateful-code-bridge.spec.ts @@ -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'); @@ -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(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(page, { path: '/api/agents', token, diff --git a/librechat.example.yaml b/librechat.example.yaml index 90c8991658d..4bc675a8e71 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -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: diff --git a/packages/api/src/admin/code.spec.ts b/packages/api/src/admin/code.spec.ts new file mode 100644 index 00000000000..c2aed879be9 --- /dev/null +++ b/packages/api/src/admin/code.spec.ts @@ -0,0 +1,140 @@ +import type { AppConfig } from '@librechat/data-schemas'; +import type { Response } from 'express'; +import type { ServerRequest } from '~/types/http'; + +import { createAdminCodeEnvironmentHandlers } from './code'; + +interface MockResponse extends Response { + statusCode: number; + body?: Record; +} + +function mockResponse(): MockResponse { + const response = { + statusCode: 200, + body: undefined, + status(code: number) { + response.statusCode = code; + return response; + }, + json(body: Record) { + response.body = body; + return response; + }, + }; + return response as unknown as MockResponse; +} + +function request(): ServerRequest { + return { + params: { environmentId: 'attached-vm' }, + user: { id: 'admin-1', role: 'ADMIN' }, + } as unknown as ServerRequest; +} + +function config(): AppConfig { + return { + endpoints: { + agents: { + statefulCodeSessions: { + allowedEnvironments: ['conversation'], + environments: [ + { + id: 'attached-vm', + name: 'Attached VM', + type: 'attached', + baseURL: 'https://bridge.example.com/v1/', + default: true, + owner: 'deployment', + pairing: { + workerId: 'vm-1', + tokenEnv: 'CODE_BRIDGE_ADMIN_TOKEN', + }, + }, + ], + }, + }, + }, + } as unknown as AppConfig; +} + +describe('createAdminCodeEnvironmentHandlers', () => { + it('creates a one-time pairing code without exposing the administrator token', async () => { + const fetchImpl = jest.fn().mockResolvedValue( + Response.json({ + protocolVersion: 1, + workerId: 'vm-1', + code: 'one-time-code-value-that-is-long', + expiresAt: '2026-08-30T12:00:00.000Z', + }), + ); + const handlers = createAdminCodeEnvironmentHandlers({ + getAppConfig: jest.fn().mockResolvedValue(config()), + readSecret: jest.fn().mockReturnValue('administrator-bootstrap-token'), + fetchImpl, + }); + const response = mockResponse(); + + await handlers.createPairing(request(), response); + + expect(fetchImpl).toHaveBeenCalledWith( + 'https://bridge.example.com/v1/bridge/pairings', + expect.objectContaining({ + method: 'POST', + headers: { + Authorization: 'Bearer administrator-bootstrap-token', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ workerId: 'vm-1' }), + }), + ); + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ + environmentId: 'attached-vm', + workerId: 'vm-1', + code: 'one-time-code-value-that-is-long', + expiresAt: '2026-08-30T12:00:00.000Z', + }); + expect(JSON.stringify(response.body)).not.toContain('administrator-bootstrap-token'); + }); + + it('fails closed before outbound traffic when the administrator token is unavailable', async () => { + const fetchImpl = jest.fn(); + const handlers = createAdminCodeEnvironmentHandlers({ + getAppConfig: jest.fn().mockResolvedValue(config()), + readSecret: jest.fn().mockReturnValue(undefined), + fetchImpl, + }); + const response = mockResponse(); + + await handlers.createPairing(request(), response); + + expect(response.statusCode).toBe(503); + expect(response.body).toEqual({ error: 'Code environment pairing is not configured' }); + expect(fetchImpl).not.toHaveBeenCalled(); + }); + + it('revokes the environment worker without returning bridge credentials', async () => { + const fetchImpl = jest.fn().mockResolvedValue(Response.json({ revoked: true })); + const handlers = createAdminCodeEnvironmentHandlers({ + getAppConfig: jest.fn().mockResolvedValue(config()), + readSecret: jest.fn().mockReturnValue('administrator-bootstrap-token'), + fetchImpl, + }); + const response = mockResponse(); + + await handlers.revokeWorker(request(), response); + + expect(fetchImpl).toHaveBeenCalledWith( + 'https://bridge.example.com/v1/bridge/workers/vm-1/revoke', + expect.objectContaining({ method: 'POST', redirect: 'error' }), + ); + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ + environmentId: 'attached-vm', + workerId: 'vm-1', + revoked: true, + }); + expect(JSON.stringify(response.body)).not.toContain('administrator-bootstrap-token'); + }); +}); diff --git a/packages/api/src/admin/code.ts b/packages/api/src/admin/code.ts new file mode 100644 index 00000000000..d524a293246 --- /dev/null +++ b/packages/api/src/admin/code.ts @@ -0,0 +1,186 @@ +import { EModelEndpoint } from 'librechat-data-provider'; + +import type { AppConfig } from '@librechat/data-schemas'; +import type { Response } from 'express'; +import type { GetAppConfigOptions } from '~/app/service'; +import type { ServerRequest } from '~/types/http'; + +import { getAppConfigOptionsFromUser } from '~/app/service'; + +const CODE_BRIDGE_REQUEST_TIMEOUT_MS = 10_000; + +type AgentsEndpointConfig = NonNullable[EModelEndpoint.agents]; +type StatefulCodeSessionsConfig = NonNullable< + NonNullable['statefulCodeSessions'] +>; +type ConfiguredCodeEnvironment = StatefulCodeSessionsConfig['environments'][number]; + +interface CodePairingResponse { + protocolVersion: number; + workerId: string; + code: string; + expiresAt: string; +} + +export interface AdminCodeEnvironmentDeps { + getAppConfig: (options: GetAppConfigOptions) => Promise; + readSecret?: (name: string) => string | undefined; + fetchImpl?: typeof fetch; +} + +function environmentId(req: ServerRequest): string { + const params = req.params as { environmentId?: string }; + return params.environmentId?.trim() ?? ''; +} + +function findEnvironment(appConfig: AppConfig, id: string): ConfiguredCodeEnvironment | undefined { + return appConfig.endpoints?.[EModelEndpoint.agents]?.statefulCodeSessions?.environments?.find( + (environment) => environment.id === id, + ); +} + +function pairingConfig(environment: ConfiguredCodeEnvironment): + | { + workerId: string; + tokenEnv: string; + } + | undefined { + if (environment.type !== 'attached' || environment.owner !== 'deployment') { + return undefined; + } + return environment.pairing; +} + +function bridgeUrl(environment: ConfiguredCodeEnvironment, path: string): string { + return `${environment.baseURL.replace(/\/+$/, '')}${path}`; +} + +function validPairingResponse(value: unknown, workerId: string): value is CodePairingResponse { + if (typeof value !== 'object' || value == null) return false; + const response = value as Partial; + return ( + response.protocolVersion === 1 && + response.workerId === workerId && + typeof response.code === 'string' && + response.code.length >= 16 && + typeof response.expiresAt === 'string' && + Number.isFinite(Date.parse(response.expiresAt)) + ); +} + +export function createAdminCodeEnvironmentHandlers(deps: AdminCodeEnvironmentDeps): { + createPairing: (req: ServerRequest, res: Response) => Promise; + revokeWorker: (req: ServerRequest, res: Response) => Promise; +} { + const fetchImpl = deps.fetchImpl ?? fetch; + const readSecret = deps.readSecret ?? ((name: string) => process.env[name]); + + async function resolve( + req: ServerRequest, + res: Response, + ): Promise< + | { + id: string; + environment: ConfiguredCodeEnvironment; + pairing: { workerId: string; tokenEnv: string }; + token: string; + } + | Response + > { + const id = environmentId(req); + const appConfig = + req.config ?? (await deps.getAppConfig(getAppConfigOptionsFromUser(req.user))); + const environment = findEnvironment(appConfig, id); + if (environment == null) { + return res.status(404).json({ error: 'Code environment was not found' }); + } + const pairing = pairingConfig(environment); + if (pairing == null) { + return res.status(409).json({ error: 'Code environment does not support pairing' }); + } + const token = readSecret(pairing.tokenEnv)?.trim(); + if (!token) { + return res.status(503).json({ error: 'Code environment pairing is not configured' }); + } + return { id, environment, pairing, token }; + } + + async function createPairing(req: ServerRequest, res: Response): Promise { + const resolved = await resolve(req, res); + if ('statusCode' in resolved) return resolved; + try { + const response = await fetchImpl(bridgeUrl(resolved.environment, '/bridge/pairings'), { + method: 'POST', + headers: { + Authorization: `Bearer ${resolved.token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ workerId: resolved.pairing.workerId }), + redirect: 'error', + signal: AbortSignal.timeout(CODE_BRIDGE_REQUEST_TIMEOUT_MS), + }); + if (!response.ok) { + return res.status(502).json({ + error: 'Code API rejected the pairing request', + upstreamStatus: response.status, + }); + } + const payload = (await response.json()) as unknown; + if (!validPairingResponse(payload, resolved.pairing.workerId)) { + return res.status(502).json({ error: 'Code API returned an invalid pairing response' }); + } + return res.status(200).json({ + environmentId: resolved.id, + workerId: payload.workerId, + code: payload.code, + expiresAt: payload.expiresAt, + }); + } catch (error) { + const timedOut = error instanceof Error && error.name === 'TimeoutError'; + return res.status(timedOut ? 504 : 502).json({ + error: timedOut ? 'Code API pairing request timed out' : 'Code API pairing request failed', + }); + } + } + + async function revokeWorker(req: ServerRequest, res: Response): Promise { + const resolved = await resolve(req, res); + if ('statusCode' in resolved) return resolved; + try { + const workerId = encodeURIComponent(resolved.pairing.workerId); + const response = await fetchImpl( + bridgeUrl(resolved.environment, `/bridge/workers/${workerId}/revoke`), + { + method: 'POST', + headers: { + Authorization: `Bearer ${resolved.token}`, + 'Content-Type': 'application/json', + }, + body: '{}', + redirect: 'error', + signal: AbortSignal.timeout(CODE_BRIDGE_REQUEST_TIMEOUT_MS), + }, + ); + if (!response.ok) { + return res.status(502).json({ + error: 'Code API rejected the revocation request', + upstreamStatus: response.status, + }); + } + return res.status(200).json({ + environmentId: resolved.id, + workerId: resolved.pairing.workerId, + revoked: true, + }); + } catch (error) { + const timedOut = error instanceof Error && error.name === 'TimeoutError'; + return res.status(timedOut ? 504 : 502).json({ + error: timedOut + ? 'Code API revocation request timed out' + : 'Code API revocation request failed', + }); + } + } + + return { createPairing, revokeWorker }; +} diff --git a/packages/api/src/admin/index.ts b/packages/api/src/admin/index.ts index 2f52e1cccff..c5259aeab0f 100644 --- a/packages/api/src/admin/index.ts +++ b/packages/api/src/admin/index.ts @@ -6,6 +6,7 @@ export { createAdminRolesHandlers } from './roles'; export { createAdminSkillsSyncAccess, createAdminSkillsSyncHandlers } from './skills'; export { createAdminUsersHandlers } from './users'; export { createAdminAuditLogHandlers } from './auditLog'; +export { createAdminCodeEnvironmentHandlers } from './code'; export { resolveConfigSecret, redactConfigSecretMaps } from './secrets'; export type { AdminConfigDeps } from './config'; export type { AdminLangfuseDeps } from './langfuse'; @@ -15,3 +16,4 @@ export type { AdminRolesDeps } from './roles'; export type { AdminSkillSyncAccessDeps, AdminSkillSyncDeps } from './skills'; export type { AdminUsersDeps } from './users'; export type { AdminAuditLogDeps } from './auditLog'; +export type { AdminCodeEnvironmentDeps } from './code'; diff --git a/packages/api/src/endpoints/config/endpoints.spec.ts b/packages/api/src/endpoints/config/endpoints.spec.ts index d0671f757a1..8e9fb15baeb 100644 --- a/packages/api/src/endpoints/config/endpoints.spec.ts +++ b/packages/api/src/endpoints/config/endpoints.spec.ts @@ -220,6 +220,57 @@ describe('createEndpointsConfigService', () => { }); }); + it('exposes pairing availability without leaking server enrollment metadata', async () => { + const deps = createMockDeps({ + loadDefaultEndpointsConfig: jest.fn().mockResolvedValue({ + [EModelEndpoint.agents]: { userProvide: false, order: 0 }, + }), + getAppConfig: jest.fn().mockResolvedValue( + appConfig({ + endpoints: { + [EModelEndpoint.agents]: { + statefulCodeSessions: { + allowedEnvironments: ['conversation'], + environments: [ + { + id: 'attached-vm', + name: 'Attached VM', + type: 'attached', + baseURL: 'https://bridge.example.com/v1', + default: true, + owner: 'deployment', + pairing: { + workerId: 'vm-1', + tokenEnv: 'CODE_BRIDGE_ADMIN_TOKEN', + }, + }, + ], + }, + }, + }, + }), + ), + }); + const { getEndpointsConfig } = createEndpointsConfigService(deps); + + const result = await getEndpointsConfig(fakeReq()); + + expect(result?.[EModelEndpoint.agents]?.statefulCodeSessions).toEqual({ + allowedEnvironments: ['conversation'], + environments: [ + { + id: 'attached-vm', + name: 'Attached VM', + type: 'attached', + baseURL: 'https://bridge.example.com/v1', + default: true, + owner: 'deployment', + pairingAvailable: true, + }, + ], + }); + }); + it('merges bedrock availableRegions', async () => { const deps = createMockDeps({ loadDefaultEndpointsConfig: jest.fn().mockResolvedValue({ diff --git a/packages/api/src/endpoints/config/endpoints.ts b/packages/api/src/endpoints/config/endpoints.ts index 4375e963126..3531ae10b93 100644 --- a/packages/api/src/endpoints/config/endpoints.ts +++ b/packages/api/src/endpoints/config/endpoints.ts @@ -76,11 +76,12 @@ export function createEndpointsConfigService(deps: EndpointsConfigDeps): { ? { allowedEnvironments: statefulCodeSessions.allowedEnvironments, environments: statefulCodeSessions.environments?.map( - ({ id, name, type, default: isDefault }) => ({ + ({ id, name, type, default: isDefault, pairing }) => ({ id, name, type, default: isDefault, + pairingAvailable: pairing != null, }), ), } diff --git a/packages/data-provider/specs/config-schemas.spec.ts b/packages/data-provider/specs/config-schemas.spec.ts index 9749f1cb43f..e52f18a0e6c 100644 --- a/packages/data-provider/specs/config-schemas.spec.ts +++ b/packages/data-provider/specs/config-schemas.spec.ts @@ -523,6 +523,62 @@ describe('agentsEndpointSchema', () => { } }); + it('accepts deployment-owned pairing configuration for an attached environment', () => { + const result = agentsEndpointSchema.safeParse({ + statefulCodeSessions: { + allowedEnvironments: ['conversation'], + environments: [ + { + id: 'attached-vm', + name: 'Attached VM', + type: 'attached', + baseURL: 'https://bridge.example.com/v1', + default: true, + owner: 'deployment', + pairing: { + workerId: 'vm-1', + tokenEnv: 'CODE_BRIDGE_ADMIN_TOKEN', + }, + }, + ], + }, + }); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.statefulCodeSessions?.environments?.[0]).toMatchObject({ + owner: 'deployment', + pairing: { + workerId: 'vm-1', + tokenEnv: 'CODE_BRIDGE_ADMIN_TOKEN', + }, + }); + } + }); + + it('rejects pairing configuration for a managed environment', () => { + const result = agentsEndpointSchema.safeParse({ + statefulCodeSessions: { + allowedEnvironments: ['conversation'], + environments: [ + { + id: 'managed', + name: 'Managed', + type: 'managed', + baseURL: 'https://code.example.com/v1', + default: true, + pairing: { + workerId: 'vm-1', + tokenEnv: 'CODE_BRIDGE_ADMIN_TOKEN', + }, + }, + ], + }, + }); + + expect(result.success).toBe(false); + }); + it('rejects ambiguous execution environment routing', () => { const environment = { id: 'attached-vm', diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 987f9d6d7ab..f6c4811f97d 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1073,6 +1073,17 @@ export const agentsEndpointSchema = baseEndpointSchema type: z.enum(['managed', 'attached']), baseURL: codeEnvironmentBaseURLSchema, default: z.boolean().optional(), + /** Ownership is explicit even though the first pairing control + * plane supports deployment-owned workers only. */ + owner: z.literal('deployment').optional().default('deployment'), + /** Server-only enrollment metadata. `tokenEnv` names an + * environment variable and never contains the token itself. */ + pairing: z + .object({ + workerId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/), + tokenEnv: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), + }) + .optional(), }), ) .optional(), @@ -1082,6 +1093,13 @@ export const agentsEndpointSchema = baseEndpointSchema const ids = new Set(); let defaults = 0; for (const environment of value.environments) { + if (environment.pairing != null && environment.type !== 'attached') { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Only attached code environments may configure pairing', + path: ['environments', environment.id, 'pairing'], + }); + } if (ids.has(environment.id)) { context.addIssue({ code: z.ZodIssueCode.custom, From 8e224dfc7fdb29681968fda2fcff198f7c22e34c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 07:52:29 -0400 Subject: [PATCH 2/8] fix: satisfy code environment type checks --- packages/api/src/admin/code.spec.ts | 2 +- packages/api/src/admin/code.ts | 8 ++++++-- packages/api/src/agents/execution.spec.ts | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/api/src/admin/code.spec.ts b/packages/api/src/admin/code.spec.ts index c2aed879be9..4ac451fcd6f 100644 --- a/packages/api/src/admin/code.spec.ts +++ b/packages/api/src/admin/code.spec.ts @@ -12,7 +12,7 @@ interface MockResponse extends Response { function mockResponse(): MockResponse { const response = { statusCode: 200, - body: undefined, + body: undefined as Record | undefined, status(code: number) { response.statusCode = code; return response; diff --git a/packages/api/src/admin/code.ts b/packages/api/src/admin/code.ts index d524a293246..4245f9e0374 100644 --- a/packages/api/src/admin/code.ts +++ b/packages/api/src/admin/code.ts @@ -13,7 +13,11 @@ type AgentsEndpointConfig = NonNullable[EModelEndpoint.a type StatefulCodeSessionsConfig = NonNullable< NonNullable['statefulCodeSessions'] >; -type ConfiguredCodeEnvironment = StatefulCodeSessionsConfig['environments'][number]; +type ConfiguredCodeEnvironment = NonNullable[number]; +type FetchImpl = ( + input: Parameters[0], + init?: Parameters[1], +) => ReturnType; interface CodePairingResponse { protocolVersion: number; @@ -25,7 +29,7 @@ interface CodePairingResponse { export interface AdminCodeEnvironmentDeps { getAppConfig: (options: GetAppConfigOptions) => Promise; readSecret?: (name: string) => string | undefined; - fetchImpl?: typeof fetch; + fetchImpl?: FetchImpl; } function environmentId(req: ServerRequest): string { diff --git a/packages/api/src/agents/execution.spec.ts b/packages/api/src/agents/execution.spec.ts index 1a6ffabc72d..9628967376c 100644 --- a/packages/api/src/agents/execution.spec.ts +++ b/packages/api/src/agents/execution.spec.ts @@ -106,12 +106,14 @@ describe('resolveCodeExecutionContext', () => { name: 'Managed', type: 'managed', baseURL: 'https://managed.example/v1', + owner: 'deployment', }, { id: 'my-vm', name: 'My VM', type: 'attached', baseURL: 'https://bridge.example/v1/', + owner: 'deployment', }, ], userId: 'user-1', @@ -166,6 +168,7 @@ describe('resolveCodeExecutionContext', () => { type: 'attached', baseURL: 'https://bridge.example/v1', default: true, + owner: 'deployment', }, ], userId: 'user-1', From 6efd32fec53a4b5e6af5e49ea9be1ec3e07b58c6 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 08:23:00 -0400 Subject: [PATCH 3/8] fix: secure code environment administration --- api/server/routes/admin/code.js | 3 +- api/server/routes/admin/code.test.js | 7 ++- packages/api/src/admin/code.spec.ts | 50 +++++++++++++++++++ packages/api/src/admin/code.ts | 8 +-- packages/api/src/agents/execution.spec.ts | 1 + .../src/endpoints/config/endpoints.spec.ts | 3 +- .../data-schemas/src/admin/capabilities.ts | 3 ++ 7 files changed, 66 insertions(+), 9 deletions(-) diff --git a/api/server/routes/admin/code.js b/api/server/routes/admin/code.js index 4adced9ed4d..455a388a03e 100644 --- a/api/server/routes/admin/code.js +++ b/api/server/routes/admin/code.js @@ -7,9 +7,10 @@ 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); +router.use(requireJwtAuth, requireAdminAccess, requireCodeEnvironmentManage); router.post('/:environmentId/pairings', handlers.createPairing); router.post('/:environmentId/revoke', handlers.revokeWorker); diff --git a/api/server/routes/admin/code.test.js b/api/server/routes/admin/code.test.js index 7f8a19c1815..3105fdc8d3d 100644 --- a/api/server/routes/admin/code.test.js +++ b/api/server/routes/admin/code.test.js @@ -22,7 +22,10 @@ const mockHandlers = { const mockGetAppConfig = jest.fn(); jest.mock('@librechat/data-schemas', () => ({ - SystemCapabilities: { ACCESS_ADMIN: 'access:admin' }, + SystemCapabilities: { + ACCESS_ADMIN: 'access:admin', + MANAGE_CODE_ENVIRONMENTS: 'manage:code_environments', + }, })); jest.mock('@librechat/api', () => ({ @@ -65,7 +68,7 @@ describe('admin code environment routes', () => { .expect(200); expect(response.body).toEqual({ operation, environmentId: 'attached-vm' }); - expect(middlewareCalls).toEqual(['jwt', 'access:admin']); + expect(middlewareCalls).toEqual(['jwt', 'access:admin', 'manage:code_environments']); expect(mockHandlers[handlerName]).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/api/src/admin/code.spec.ts b/packages/api/src/admin/code.spec.ts index 4ac451fcd6f..e384a05e2c4 100644 --- a/packages/api/src/admin/code.spec.ts +++ b/packages/api/src/admin/code.spec.ts @@ -98,6 +98,56 @@ describe('createAdminCodeEnvironmentHandlers', () => { expect(JSON.stringify(response.body)).not.toContain('administrator-bootstrap-token'); }); + it('ignores caller-specific config when resolving pairing secrets and destinations', async () => { + const getAppConfig = jest.fn().mockResolvedValue(config()); + const readSecret = jest.fn((name: string) => + name === 'CODE_BRIDGE_ADMIN_TOKEN' ? 'deployment-token' : 'sensitive-database-secret', + ); + const fetchImpl = jest.fn().mockResolvedValue( + Response.json({ + protocolVersion: 1, + workerId: 'vm-1', + code: 'one-time-code-value-that-is-long', + expiresAt: '2026-08-30T12:00:00.000Z', + }), + ); + const req = request(); + req.config = { + endpoints: { + agents: { + statefulCodeSessions: { + allowedEnvironments: ['conversation'], + environments: [ + { + id: 'attached-vm', + name: 'Malicious Override', + type: 'attached', + baseURL: 'https://attacker.example.com/v1', + owner: 'deployment', + pairing: { workerId: 'vm-1', tokenEnv: 'DATABASE_URL' }, + }, + ], + }, + }, + }, + } as AppConfig; + const handlers = createAdminCodeEnvironmentHandlers({ + getAppConfig, + readSecret, + fetchImpl, + }); + + await handlers.createPairing(req, mockResponse()); + + expect(getAppConfig).toHaveBeenCalledWith({ tenantId: undefined }); + expect(readSecret).toHaveBeenCalledWith('CODE_BRIDGE_ADMIN_TOKEN'); + expect(readSecret).not.toHaveBeenCalledWith('DATABASE_URL'); + expect(fetchImpl).toHaveBeenCalledWith( + 'https://bridge.example.com/v1/bridge/pairings', + expect.any(Object), + ); + }); + it('fails closed before outbound traffic when the administrator token is unavailable', async () => { const fetchImpl = jest.fn(); const handlers = createAdminCodeEnvironmentHandlers({ diff --git a/packages/api/src/admin/code.ts b/packages/api/src/admin/code.ts index 4245f9e0374..9cc05b50718 100644 --- a/packages/api/src/admin/code.ts +++ b/packages/api/src/admin/code.ts @@ -5,8 +5,6 @@ import type { Response } from 'express'; import type { GetAppConfigOptions } from '~/app/service'; import type { ServerRequest } from '~/types/http'; -import { getAppConfigOptionsFromUser } from '~/app/service'; - const CODE_BRIDGE_REQUEST_TIMEOUT_MS = 10_000; type AgentsEndpointConfig = NonNullable[EModelEndpoint.agents]; @@ -92,8 +90,10 @@ export function createAdminCodeEnvironmentHandlers(deps: AdminCodeEnvironmentDep | Response > { const id = environmentId(req); - const appConfig = - req.config ?? (await deps.getAppConfig(getAppConfigOptionsFromUser(req.user))); + /** Pairing credentials are deployment control-plane state. Resolve only + * the tenant's YAML + __base__ configuration; user, role, and group + * overrides must never choose tokenEnv or the outbound destination. */ + const appConfig = await deps.getAppConfig({ tenantId: req.user?.tenantId }); const environment = findEnvironment(appConfig, id); if (environment == null) { return res.status(404).json({ error: 'Code environment was not found' }); diff --git a/packages/api/src/agents/execution.spec.ts b/packages/api/src/agents/execution.spec.ts index 9628967376c..859e231d33e 100644 --- a/packages/api/src/agents/execution.spec.ts +++ b/packages/api/src/agents/execution.spec.ts @@ -139,6 +139,7 @@ describe('resolveCodeExecutionContext', () => { type: 'attached' as const, baseURL, default: true, + owner: 'deployment' as const, }); const first = resolveCodeExecutionContext({ statefulSessions: true, diff --git a/packages/api/src/endpoints/config/endpoints.spec.ts b/packages/api/src/endpoints/config/endpoints.spec.ts index 8e9fb15baeb..2d5ecce63b0 100644 --- a/packages/api/src/endpoints/config/endpoints.spec.ts +++ b/packages/api/src/endpoints/config/endpoints.spec.ts @@ -215,6 +215,7 @@ describe('createEndpointsConfigService', () => { name: 'Attached VM', type: 'attached', default: true, + pairingAvailable: false, }, ], }); @@ -262,9 +263,7 @@ describe('createEndpointsConfigService', () => { id: 'attached-vm', name: 'Attached VM', type: 'attached', - baseURL: 'https://bridge.example.com/v1', default: true, - owner: 'deployment', pairingAvailable: true, }, ], diff --git a/packages/data-schemas/src/admin/capabilities.ts b/packages/data-schemas/src/admin/capabilities.ts index 1cca28804ec..d9492655afa 100644 --- a/packages/data-schemas/src/admin/capabilities.ts +++ b/packages/data-schemas/src/admin/capabilities.ts @@ -29,6 +29,8 @@ export const SystemCapabilities = { READ_AGENTS: 'read:agents', MANAGE_AGENTS: 'manage:agents', MANAGE_MCP_SERVERS: 'manage:mcpservers', + /** Enrolls and revokes deployment-owned Code API workers. */ + MANAGE_CODE_ENVIRONMENTS: 'manage:code_environments', READ_PROMPTS: 'read:prompts', MANAGE_PROMPTS: 'manage:prompts', READ_SKILLS: 'read:skills', @@ -252,6 +254,7 @@ export const CAPABILITY_CATEGORIES: CapabilityCategory[] = [ SystemCapabilities.MANAGE_ASSISTANTS, SystemCapabilities.READ_ASSISTANTS, SystemCapabilities.MANAGE_MCP_SERVERS, + SystemCapabilities.MANAGE_CODE_ENVIRONMENTS, SystemCapabilities.MANAGE_SHARED_LINKS, SystemCapabilities.READ_SHARED_LINKS, ], From 1ebdeef6872568e112374625f428cddc3a988a5d Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 08:36:18 -0400 Subject: [PATCH 4/8] fix: isolate code pairing control plane --- packages/api/src/admin/code.spec.ts | 38 +++++++++++------------------ packages/api/src/admin/code.ts | 6 ++--- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/packages/api/src/admin/code.spec.ts b/packages/api/src/admin/code.spec.ts index e384a05e2c4..aa47a65c287 100644 --- a/packages/api/src/admin/code.spec.ts +++ b/packages/api/src/admin/code.spec.ts @@ -98,8 +98,18 @@ describe('createAdminCodeEnvironmentHandlers', () => { expect(JSON.stringify(response.body)).not.toContain('administrator-bootstrap-token'); }); - it('ignores caller-specific config when resolving pairing secrets and destinations', async () => { - const getAppConfig = jest.fn().mockResolvedValue(config()); + it('uses only YAML config when resolving pairing secrets and destinations', async () => { + const writableOverride = config(); + const overriddenEnvironment = + writableOverride.endpoints?.agents?.statefulCodeSessions?.environments?.[0]; + if (overriddenEnvironment == null) { + throw new Error('Expected the test code environment'); + } + overriddenEnvironment.baseURL = 'https://attacker.example.com/v1'; + overriddenEnvironment.pairing = { workerId: 'vm-1', tokenEnv: 'DATABASE_URL' }; + const getAppConfig = jest.fn(async (options: { baseOnly?: boolean }) => + options.baseOnly === true ? config() : writableOverride, + ); const readSecret = jest.fn((name: string) => name === 'CODE_BRIDGE_ADMIN_TOKEN' ? 'deployment-token' : 'sensitive-database-secret', ); @@ -111,35 +121,15 @@ describe('createAdminCodeEnvironmentHandlers', () => { expiresAt: '2026-08-30T12:00:00.000Z', }), ); - const req = request(); - req.config = { - endpoints: { - agents: { - statefulCodeSessions: { - allowedEnvironments: ['conversation'], - environments: [ - { - id: 'attached-vm', - name: 'Malicious Override', - type: 'attached', - baseURL: 'https://attacker.example.com/v1', - owner: 'deployment', - pairing: { workerId: 'vm-1', tokenEnv: 'DATABASE_URL' }, - }, - ], - }, - }, - }, - } as AppConfig; const handlers = createAdminCodeEnvironmentHandlers({ getAppConfig, readSecret, fetchImpl, }); - await handlers.createPairing(req, mockResponse()); + await handlers.createPairing(request(), mockResponse()); - expect(getAppConfig).toHaveBeenCalledWith({ tenantId: undefined }); + expect(getAppConfig).toHaveBeenCalledWith({ baseOnly: true }); expect(readSecret).toHaveBeenCalledWith('CODE_BRIDGE_ADMIN_TOKEN'); expect(readSecret).not.toHaveBeenCalledWith('DATABASE_URL'); expect(fetchImpl).toHaveBeenCalledWith( diff --git a/packages/api/src/admin/code.ts b/packages/api/src/admin/code.ts index 9cc05b50718..b0aeadcb80a 100644 --- a/packages/api/src/admin/code.ts +++ b/packages/api/src/admin/code.ts @@ -91,9 +91,9 @@ export function createAdminCodeEnvironmentHandlers(deps: AdminCodeEnvironmentDep > { const id = environmentId(req); /** Pairing credentials are deployment control-plane state. Resolve only - * the tenant's YAML + __base__ configuration; user, role, and group - * overrides must never choose tokenEnv or the outbound destination. */ - const appConfig = await deps.getAppConfig({ tenantId: req.user?.tenantId }); + * YAML-backed configuration; writable database overrides must never + * choose tokenEnv or the outbound destination. */ + const appConfig = await deps.getAppConfig({ baseOnly: true }); const environment = findEnvironment(appConfig, id); if (environment == null) { return res.status(404).json({ error: 'Code environment was not found' }); From d38905fb470b8a744fd01fda0320f26026e20739 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 09:27:12 -0400 Subject: [PATCH 5/8] fix: validate code pairing control responses --- packages/api/src/admin/code.spec.ts | 49 +++++++++++++-- packages/api/src/admin/code.ts | 20 ++++++- .../src/endpoints/config/endpoints.spec.ts | 59 +++++++++++++++++++ .../api/src/endpoints/config/endpoints.ts | 17 +++++- 4 files changed, 137 insertions(+), 8 deletions(-) diff --git a/packages/api/src/admin/code.spec.ts b/packages/api/src/admin/code.spec.ts index aa47a65c287..c7853333a2e 100644 --- a/packages/api/src/admin/code.spec.ts +++ b/packages/api/src/admin/code.spec.ts @@ -65,7 +65,7 @@ describe('createAdminCodeEnvironmentHandlers', () => { protocolVersion: 1, workerId: 'vm-1', code: 'one-time-code-value-that-is-long', - expiresAt: '2026-08-30T12:00:00.000Z', + expiresAt: '2099-08-30T12:00:00.000Z', }), ); const handlers = createAdminCodeEnvironmentHandlers({ @@ -93,7 +93,7 @@ describe('createAdminCodeEnvironmentHandlers', () => { environmentId: 'attached-vm', workerId: 'vm-1', code: 'one-time-code-value-that-is-long', - expiresAt: '2026-08-30T12:00:00.000Z', + expiresAt: '2099-08-30T12:00:00.000Z', }); expect(JSON.stringify(response.body)).not.toContain('administrator-bootstrap-token'); }); @@ -118,7 +118,7 @@ describe('createAdminCodeEnvironmentHandlers', () => { protocolVersion: 1, workerId: 'vm-1', code: 'one-time-code-value-that-is-long', - expiresAt: '2026-08-30T12:00:00.000Z', + expiresAt: '2099-08-30T12:00:00.000Z', }), ); const handlers = createAdminCodeEnvironmentHandlers({ @@ -154,8 +154,32 @@ describe('createAdminCodeEnvironmentHandlers', () => { expect(fetchImpl).not.toHaveBeenCalled(); }); + it('rejects an expired one-time pairing code', async () => { + const fetchImpl = jest.fn().mockResolvedValue( + Response.json({ + protocolVersion: 1, + workerId: 'vm-1', + code: 'expired-one-time-code-value', + expiresAt: '2000-01-01T00:00:00.000Z', + }), + ); + const handlers = createAdminCodeEnvironmentHandlers({ + getAppConfig: jest.fn().mockResolvedValue(config()), + readSecret: jest.fn().mockReturnValue('administrator-bootstrap-token'), + fetchImpl, + }); + const response = mockResponse(); + + await handlers.createPairing(request(), response); + + expect(response.statusCode).toBe(502); + expect(response.body).toEqual({ error: 'Code API returned an invalid pairing response' }); + }); + it('revokes the environment worker without returning bridge credentials', async () => { - const fetchImpl = jest.fn().mockResolvedValue(Response.json({ revoked: true })); + const fetchImpl = jest + .fn() + .mockResolvedValue(Response.json({ protocolVersion: 1, revoked: true })); const handlers = createAdminCodeEnvironmentHandlers({ getAppConfig: jest.fn().mockResolvedValue(config()), readSecret: jest.fn().mockReturnValue('administrator-bootstrap-token'), @@ -177,4 +201,21 @@ describe('createAdminCodeEnvironmentHandlers', () => { }); expect(JSON.stringify(response.body)).not.toContain('administrator-bootstrap-token'); }); + + it('rejects an invalid revocation acknowledgement', async () => { + const fetchImpl = jest + .fn() + .mockResolvedValue(Response.json({ protocolVersion: 1, revoked: false })); + const handlers = createAdminCodeEnvironmentHandlers({ + getAppConfig: jest.fn().mockResolvedValue(config()), + readSecret: jest.fn().mockReturnValue('administrator-bootstrap-token'), + fetchImpl, + }); + const response = mockResponse(); + + await handlers.revokeWorker(request(), response); + + expect(response.statusCode).toBe(502); + expect(response.body).toEqual({ error: 'Code API returned an invalid revocation response' }); + }); }); diff --git a/packages/api/src/admin/code.ts b/packages/api/src/admin/code.ts index b0aeadcb80a..fbf69bc4a5e 100644 --- a/packages/api/src/admin/code.ts +++ b/packages/api/src/admin/code.ts @@ -24,6 +24,11 @@ interface CodePairingResponse { expiresAt: string; } +interface CodeRevocationResponse { + protocolVersion: number; + revoked: true; +} + export interface AdminCodeEnvironmentDeps { getAppConfig: (options: GetAppConfigOptions) => Promise; readSecret?: (name: string) => string | undefined; @@ -60,16 +65,23 @@ function bridgeUrl(environment: ConfiguredCodeEnvironment, path: string): string function validPairingResponse(value: unknown, workerId: string): value is CodePairingResponse { if (typeof value !== 'object' || value == null) return false; const response = value as Partial; + const expiresAt = typeof response.expiresAt === 'string' ? Date.parse(response.expiresAt) : NaN; return ( response.protocolVersion === 1 && response.workerId === workerId && typeof response.code === 'string' && response.code.length >= 16 && - typeof response.expiresAt === 'string' && - Number.isFinite(Date.parse(response.expiresAt)) + Number.isFinite(expiresAt) && + expiresAt > Date.now() ); } +function validRevocationResponse(value: unknown): value is CodeRevocationResponse { + if (typeof value !== 'object' || value == null) return false; + const response = value as Partial; + return response.protocolVersion === 1 && response.revoked === true; +} + export function createAdminCodeEnvironmentHandlers(deps: AdminCodeEnvironmentDeps): { createPairing: (req: ServerRequest, res: Response) => Promise; revokeWorker: (req: ServerRequest, res: Response) => Promise; @@ -171,6 +183,10 @@ export function createAdminCodeEnvironmentHandlers(deps: AdminCodeEnvironmentDep upstreamStatus: response.status, }); } + const payload = (await response.json()) as unknown; + if (!validRevocationResponse(payload)) { + return res.status(502).json({ error: 'Code API returned an invalid revocation response' }); + } return res.status(200).json({ environmentId: resolved.id, workerId: resolved.pairing.workerId, diff --git a/packages/api/src/endpoints/config/endpoints.spec.ts b/packages/api/src/endpoints/config/endpoints.spec.ts index 2d5ecce63b0..a25f38102ad 100644 --- a/packages/api/src/endpoints/config/endpoints.spec.ts +++ b/packages/api/src/endpoints/config/endpoints.spec.ts @@ -270,6 +270,65 @@ describe('createEndpointsConfigService', () => { }); }); + it('does not advertise pairing configured only by a writable override', async () => { + const effectiveConfig = appConfig({ + endpoints: { + [EModelEndpoint.agents]: { + statefulCodeSessions: { + allowedEnvironments: ['conversation'], + environments: [ + { + id: 'attached-vm', + name: 'Attached VM', + type: 'attached', + baseURL: 'https://override.example.com/v1', + owner: 'deployment', + pairing: { + workerId: 'vm-1', + tokenEnv: 'DATABASE_URL', + }, + }, + ], + }, + }, + }, + }); + const getAppConfig = jest.fn().mockResolvedValue( + appConfig({ + endpoints: { + [EModelEndpoint.agents]: { + statefulCodeSessions: { + allowedEnvironments: ['conversation'], + environments: [ + { + id: 'attached-vm', + name: 'Attached VM', + type: 'attached', + baseURL: 'https://bridge.example.com/v1', + owner: 'deployment', + }, + ], + }, + }, + }, + }), + ); + const deps = createMockDeps({ + getAppConfig, + loadDefaultEndpointsConfig: jest.fn().mockResolvedValue({ + [EModelEndpoint.agents]: { userProvide: false, order: 0 }, + }), + }); + const { getEndpointsConfig } = createEndpointsConfigService(deps); + + const result = await getEndpointsConfig(fakeReq({ config: effectiveConfig })); + + expect(getAppConfig).toHaveBeenCalledWith({ baseOnly: true }); + expect( + result?.[EModelEndpoint.agents]?.statefulCodeSessions?.environments?.[0]?.pairingAvailable, + ).toBe(false); + }); + it('merges bedrock availableRegions', async () => { const deps = createMockDeps({ loadDefaultEndpointsConfig: jest.fn().mockResolvedValue({ diff --git a/packages/api/src/endpoints/config/endpoints.ts b/packages/api/src/endpoints/config/endpoints.ts index 3531ae10b93..0ce6aeefeaa 100644 --- a/packages/api/src/endpoints/config/endpoints.ts +++ b/packages/api/src/endpoints/config/endpoints.ts @@ -72,16 +72,29 @@ export function createEndpointsConfigService(deps: EndpointsConfigDeps): { if (mergedConfig[EModelEndpoint.agents] && appConfig?.endpoints?.[EModelEndpoint.agents]) { const { disableBuilder, capabilities, allowedProviders, statefulCodeSessions, maxSubagents } = appConfig.endpoints[EModelEndpoint.agents]; + const deploymentConfig = statefulCodeSessions?.environments?.length + ? await getAppConfig({ baseOnly: true }) + : undefined; + const pairableEnvironmentIds = new Set( + deploymentConfig?.endpoints?.[EModelEndpoint.agents]?.statefulCodeSessions?.environments + ?.filter( + (environment) => + environment.type === 'attached' && + environment.owner === 'deployment' && + environment.pairing != null, + ) + .map((environment) => environment.id), + ); const clientStatefulCodeSessions = statefulCodeSessions ? { allowedEnvironments: statefulCodeSessions.allowedEnvironments, environments: statefulCodeSessions.environments?.map( - ({ id, name, type, default: isDefault, pairing }) => ({ + ({ id, name, type, default: isDefault }) => ({ id, name, type, default: isDefault, - pairingAvailable: pairing != null, + pairingAvailable: pairableEnvironmentIds.has(id), }), ), } From d1516837f5a97a305b77a26de79096ea3927efee Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 10:00:33 -0400 Subject: [PATCH 6/8] fix: secure code pairing transport --- packages/api/src/admin/code.spec.ts | 51 ++++++++ packages/api/src/admin/code.ts | 7 +- .../src/endpoints/config/endpoints.spec.ts | 109 ------------------ .../api/src/endpoints/config/endpoints.ts | 14 --- .../specs/config-schemas.spec.ts | 46 ++++++++ packages/data-provider/src/config.ts | 21 ++++ 6 files changed, 123 insertions(+), 125 deletions(-) diff --git a/packages/api/src/admin/code.spec.ts b/packages/api/src/admin/code.spec.ts index c7853333a2e..d99c455e60a 100644 --- a/packages/api/src/admin/code.spec.ts +++ b/packages/api/src/admin/code.spec.ts @@ -154,6 +154,57 @@ describe('createAdminCodeEnvironmentHandlers', () => { expect(fetchImpl).not.toHaveBeenCalled(); }); + it('normalizes the bridge base URL before appending control paths', async () => { + const deploymentConfig = config(); + const environment = deploymentConfig.endpoints?.agents?.statefulCodeSessions?.environments?.[0]; + if (environment == null) throw new Error('Expected the test code environment'); + environment.baseURL = ' https://bridge.example.com/v1/ '; + const fetchImpl = jest.fn().mockResolvedValue( + Response.json({ + protocolVersion: 1, + workerId: 'vm-1', + code: 'one-time-code-value-that-is-long', + expiresAt: '2099-08-30T12:00:00.000Z', + }), + ); + const handlers = createAdminCodeEnvironmentHandlers({ + getAppConfig: jest.fn().mockResolvedValue(deploymentConfig), + readSecret: jest.fn().mockReturnValue('administrator-bootstrap-token'), + fetchImpl, + }); + + await handlers.createPairing(request(), mockResponse()); + + expect(fetchImpl).toHaveBeenCalledWith( + 'https://bridge.example.com/v1/bridge/pairings', + expect.any(Object), + ); + }); + + it('rejects insecure non-loopback pairing before reading or sending credentials', async () => { + const deploymentConfig = config(); + const environment = deploymentConfig.endpoints?.agents?.statefulCodeSessions?.environments?.[0]; + if (environment == null) throw new Error('Expected the test code environment'); + environment.baseURL = 'http://bridge.example.com/v1'; + const readSecret = jest.fn().mockReturnValue('administrator-bootstrap-token'); + const fetchImpl = jest.fn(); + const handlers = createAdminCodeEnvironmentHandlers({ + getAppConfig: jest.fn().mockResolvedValue(deploymentConfig), + readSecret, + fetchImpl, + }); + const response = mockResponse(); + + await handlers.createPairing(request(), response); + + expect(response.statusCode).toBe(409); + expect(response.body).toEqual({ + error: 'Code environment pairing requires secure transport', + }); + expect(readSecret).not.toHaveBeenCalled(); + expect(fetchImpl).not.toHaveBeenCalled(); + }); + it('rejects an expired one-time pairing code', async () => { const fetchImpl = jest.fn().mockResolvedValue( Response.json({ diff --git a/packages/api/src/admin/code.ts b/packages/api/src/admin/code.ts index fbf69bc4a5e..106769c2810 100644 --- a/packages/api/src/admin/code.ts +++ b/packages/api/src/admin/code.ts @@ -1,4 +1,4 @@ -import { EModelEndpoint } from 'librechat-data-provider'; +import { EModelEndpoint, isSecureCodeEnvironmentControlURL } from 'librechat-data-provider'; import type { AppConfig } from '@librechat/data-schemas'; import type { Response } from 'express'; @@ -59,7 +59,7 @@ function pairingConfig(environment: ConfiguredCodeEnvironment): } function bridgeUrl(environment: ConfiguredCodeEnvironment, path: string): string { - return `${environment.baseURL.replace(/\/+$/, '')}${path}`; + return `${environment.baseURL.trim().replace(/\/+$/, '')}${path}`; } function validPairingResponse(value: unknown, workerId: string): value is CodePairingResponse { @@ -114,6 +114,9 @@ export function createAdminCodeEnvironmentHandlers(deps: AdminCodeEnvironmentDep if (pairing == null) { return res.status(409).json({ error: 'Code environment does not support pairing' }); } + if (!isSecureCodeEnvironmentControlURL(environment.baseURL)) { + return res.status(409).json({ error: 'Code environment pairing requires secure transport' }); + } const token = readSecret(pairing.tokenEnv)?.trim(); if (!token) { return res.status(503).json({ error: 'Code environment pairing is not configured' }); diff --git a/packages/api/src/endpoints/config/endpoints.spec.ts b/packages/api/src/endpoints/config/endpoints.spec.ts index a25f38102ad..d0671f757a1 100644 --- a/packages/api/src/endpoints/config/endpoints.spec.ts +++ b/packages/api/src/endpoints/config/endpoints.spec.ts @@ -215,120 +215,11 @@ describe('createEndpointsConfigService', () => { name: 'Attached VM', type: 'attached', default: true, - pairingAvailable: false, }, ], }); }); - it('exposes pairing availability without leaking server enrollment metadata', async () => { - const deps = createMockDeps({ - loadDefaultEndpointsConfig: jest.fn().mockResolvedValue({ - [EModelEndpoint.agents]: { userProvide: false, order: 0 }, - }), - getAppConfig: jest.fn().mockResolvedValue( - appConfig({ - endpoints: { - [EModelEndpoint.agents]: { - statefulCodeSessions: { - allowedEnvironments: ['conversation'], - environments: [ - { - id: 'attached-vm', - name: 'Attached VM', - type: 'attached', - baseURL: 'https://bridge.example.com/v1', - default: true, - owner: 'deployment', - pairing: { - workerId: 'vm-1', - tokenEnv: 'CODE_BRIDGE_ADMIN_TOKEN', - }, - }, - ], - }, - }, - }, - }), - ), - }); - const { getEndpointsConfig } = createEndpointsConfigService(deps); - - const result = await getEndpointsConfig(fakeReq()); - - expect(result?.[EModelEndpoint.agents]?.statefulCodeSessions).toEqual({ - allowedEnvironments: ['conversation'], - environments: [ - { - id: 'attached-vm', - name: 'Attached VM', - type: 'attached', - default: true, - pairingAvailable: true, - }, - ], - }); - }); - - it('does not advertise pairing configured only by a writable override', async () => { - const effectiveConfig = appConfig({ - endpoints: { - [EModelEndpoint.agents]: { - statefulCodeSessions: { - allowedEnvironments: ['conversation'], - environments: [ - { - id: 'attached-vm', - name: 'Attached VM', - type: 'attached', - baseURL: 'https://override.example.com/v1', - owner: 'deployment', - pairing: { - workerId: 'vm-1', - tokenEnv: 'DATABASE_URL', - }, - }, - ], - }, - }, - }, - }); - const getAppConfig = jest.fn().mockResolvedValue( - appConfig({ - endpoints: { - [EModelEndpoint.agents]: { - statefulCodeSessions: { - allowedEnvironments: ['conversation'], - environments: [ - { - id: 'attached-vm', - name: 'Attached VM', - type: 'attached', - baseURL: 'https://bridge.example.com/v1', - owner: 'deployment', - }, - ], - }, - }, - }, - }), - ); - const deps = createMockDeps({ - getAppConfig, - loadDefaultEndpointsConfig: jest.fn().mockResolvedValue({ - [EModelEndpoint.agents]: { userProvide: false, order: 0 }, - }), - }); - const { getEndpointsConfig } = createEndpointsConfigService(deps); - - const result = await getEndpointsConfig(fakeReq({ config: effectiveConfig })); - - expect(getAppConfig).toHaveBeenCalledWith({ baseOnly: true }); - expect( - result?.[EModelEndpoint.agents]?.statefulCodeSessions?.environments?.[0]?.pairingAvailable, - ).toBe(false); - }); - it('merges bedrock availableRegions', async () => { const deps = createMockDeps({ loadDefaultEndpointsConfig: jest.fn().mockResolvedValue({ diff --git a/packages/api/src/endpoints/config/endpoints.ts b/packages/api/src/endpoints/config/endpoints.ts index 0ce6aeefeaa..4375e963126 100644 --- a/packages/api/src/endpoints/config/endpoints.ts +++ b/packages/api/src/endpoints/config/endpoints.ts @@ -72,19 +72,6 @@ export function createEndpointsConfigService(deps: EndpointsConfigDeps): { if (mergedConfig[EModelEndpoint.agents] && appConfig?.endpoints?.[EModelEndpoint.agents]) { const { disableBuilder, capabilities, allowedProviders, statefulCodeSessions, maxSubagents } = appConfig.endpoints[EModelEndpoint.agents]; - const deploymentConfig = statefulCodeSessions?.environments?.length - ? await getAppConfig({ baseOnly: true }) - : undefined; - const pairableEnvironmentIds = new Set( - deploymentConfig?.endpoints?.[EModelEndpoint.agents]?.statefulCodeSessions?.environments - ?.filter( - (environment) => - environment.type === 'attached' && - environment.owner === 'deployment' && - environment.pairing != null, - ) - .map((environment) => environment.id), - ); const clientStatefulCodeSessions = statefulCodeSessions ? { allowedEnvironments: statefulCodeSessions.allowedEnvironments, @@ -94,7 +81,6 @@ export function createEndpointsConfigService(deps: EndpointsConfigDeps): { name, type, default: isDefault, - pairingAvailable: pairableEnvironmentIds.has(id), }), ), } diff --git a/packages/data-provider/specs/config-schemas.spec.ts b/packages/data-provider/specs/config-schemas.spec.ts index e52f18a0e6c..33c4fc8ba1f 100644 --- a/packages/data-provider/specs/config-schemas.spec.ts +++ b/packages/data-provider/specs/config-schemas.spec.ts @@ -579,6 +579,52 @@ describe('agentsEndpointSchema', () => { expect(result.success).toBe(false); }); + it('rejects pairing over insecure non-loopback transport', () => { + const result = agentsEndpointSchema.safeParse({ + statefulCodeSessions: { + allowedEnvironments: ['conversation'], + environments: [ + { + id: 'attached-vm', + name: 'Attached VM', + type: 'attached', + baseURL: 'http://bridge.example.com/v1', + default: true, + pairing: { + workerId: 'vm-1', + tokenEnv: 'CODE_BRIDGE_ADMIN_TOKEN', + }, + }, + ], + }, + }); + + expect(result.success).toBe(false); + }); + + it('allows loopback HTTP pairing for local development', () => { + const result = agentsEndpointSchema.safeParse({ + statefulCodeSessions: { + allowedEnvironments: ['conversation'], + environments: [ + { + id: 'attached-vm', + name: 'Attached VM', + type: 'attached', + baseURL: 'http://127.0.0.1:23112/v1', + default: true, + pairing: { + workerId: 'vm-1', + tokenEnv: 'CODE_BRIDGE_ADMIN_TOKEN', + }, + }, + ], + }, + }); + + expect(result.success).toBe(true); + }); + it('rejects ambiguous execution environment routing', () => { const environment = { id: 'attached-vm', diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index f6c4811f97d..b0f7a97eefb 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1023,6 +1023,17 @@ const codeEnvironmentBaseURLSchema = z { message: 'Code environment baseURL must be an HTTP(S) base URL without query or fragment' }, ); +export function isSecureCodeEnvironmentControlURL(baseURL: string): boolean { + try { + const url = new URL(baseURL.trim()); + if (url.protocol === 'https:') return true; + if (url.protocol !== 'http:') return false; + return url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]'; + } catch { + return false; + } +} + export const agentsEndpointSchema = baseEndpointSchema .omit({ baseURL: true }) .merge( @@ -1100,6 +1111,16 @@ export const agentsEndpointSchema = baseEndpointSchema path: ['environments', environment.id, 'pairing'], }); } + if ( + environment.pairing != null && + !isSecureCodeEnvironmentControlURL(environment.baseURL) + ) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Paired code environments require HTTPS outside loopback development', + path: ['environments', environment.id, 'baseURL'], + }); + } if (ids.has(environment.id)) { context.addIssue({ code: z.ZodIssueCode.custom, From 87ca98ee94246647df46db20ea6b5724cb8c0960 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 10:08:38 -0400 Subject: [PATCH 7/8] fix: validate code pairing wire format --- packages/api/src/admin/code.spec.ts | 22 ++++++++++++++++++++++ packages/api/src/admin/code.ts | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/api/src/admin/code.spec.ts b/packages/api/src/admin/code.spec.ts index d99c455e60a..cb4c0290b51 100644 --- a/packages/api/src/admin/code.spec.ts +++ b/packages/api/src/admin/code.spec.ts @@ -227,6 +227,28 @@ describe('createAdminCodeEnvironmentHandlers', () => { expect(response.body).toEqual({ error: 'Code API returned an invalid pairing response' }); }); + it('rejects a pairing code outside the 32-character base64url wire format', async () => { + const fetchImpl = jest.fn().mockResolvedValue( + Response.json({ + protocolVersion: 1, + workerId: 'vm-1', + code: 'invalid code value with whitespace', + expiresAt: '2099-08-30T12:00:00.000Z', + }), + ); + const handlers = createAdminCodeEnvironmentHandlers({ + getAppConfig: jest.fn().mockResolvedValue(config()), + readSecret: jest.fn().mockReturnValue('administrator-bootstrap-token'), + fetchImpl, + }); + const response = mockResponse(); + + await handlers.createPairing(request(), response); + + expect(response.statusCode).toBe(502); + expect(response.body).toEqual({ error: 'Code API returned an invalid pairing response' }); + }); + it('revokes the environment worker without returning bridge credentials', async () => { const fetchImpl = jest .fn() diff --git a/packages/api/src/admin/code.ts b/packages/api/src/admin/code.ts index 106769c2810..867d40ab5cf 100644 --- a/packages/api/src/admin/code.ts +++ b/packages/api/src/admin/code.ts @@ -70,7 +70,7 @@ function validPairingResponse(value: unknown, workerId: string): value is CodePa response.protocolVersion === 1 && response.workerId === workerId && typeof response.code === 'string' && - response.code.length >= 16 && + /^[A-Za-z0-9_-]{32}$/.test(response.code) && Number.isFinite(expiresAt) && expiresAt > Date.now() ); From e15b816058d24e4cd460c6420a02f40a2b781db1 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 10:21:51 -0400 Subject: [PATCH 8/8] fix: harden pairing secret lookup --- packages/api/src/admin/code.spec.ts | 19 +++++++++++++++++++ packages/api/src/admin/code.ts | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/api/src/admin/code.spec.ts b/packages/api/src/admin/code.spec.ts index cb4c0290b51..38b22c7deb6 100644 --- a/packages/api/src/admin/code.spec.ts +++ b/packages/api/src/admin/code.spec.ts @@ -154,6 +154,25 @@ describe('createAdminCodeEnvironmentHandlers', () => { expect(fetchImpl).not.toHaveBeenCalled(); }); + it('treats inherited process environment properties as missing secrets', async () => { + const deploymentConfig = config(); + const environment = deploymentConfig.endpoints?.agents?.statefulCodeSessions?.environments?.[0]; + if (environment?.pairing == null) throw new Error('Expected the test pairing configuration'); + environment.pairing.tokenEnv = 'constructor'; + const fetchImpl = jest.fn(); + const handlers = createAdminCodeEnvironmentHandlers({ + getAppConfig: jest.fn().mockResolvedValue(deploymentConfig), + fetchImpl, + }); + const response = mockResponse(); + + await handlers.createPairing(request(), response); + + expect(response.statusCode).toBe(503); + expect(response.body).toEqual({ error: 'Code environment pairing is not configured' }); + expect(fetchImpl).not.toHaveBeenCalled(); + }); + it('normalizes the bridge base URL before appending control paths', async () => { const deploymentConfig = config(); const environment = deploymentConfig.endpoints?.agents?.statefulCodeSessions?.environments?.[0]; diff --git a/packages/api/src/admin/code.ts b/packages/api/src/admin/code.ts index 867d40ab5cf..f84c45a177c 100644 --- a/packages/api/src/admin/code.ts +++ b/packages/api/src/admin/code.ts @@ -87,7 +87,10 @@ export function createAdminCodeEnvironmentHandlers(deps: AdminCodeEnvironmentDep revokeWorker: (req: ServerRequest, res: Response) => Promise; } { const fetchImpl = deps.fetchImpl ?? fetch; - const readSecret = deps.readSecret ?? ((name: string) => process.env[name]); + const readSecret = + deps.readSecret ?? + ((name: string) => + Object.prototype.hasOwnProperty.call(process.env, name) ? process.env[name] : undefined); async function resolve( req: ServerRequest,