From 82040ea5c69d95b0d77d86d35fdfaa41d724ece3 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 22:33:47 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Thread=20req=20into=20liv?= =?UTF-8?q?eness=20checks=20+=20skip=20credential-less=20staleness=20probe?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit initializeAgent primed resources with principal only, so checkSessionsAlive minted JWT headers from undefined and every ref older than the 6h window failed its liveness check unauthorized, churning live sandbox files through re-provisioning each turn. req now flows through primeResources (adopting the canonical ~/types ServerRequest), and the staleness probe only runs when it can actually authenticate: a legacy key, or a req to mint bearer auth from. --- packages/api/src/agents/initialize.ts | 1 + packages/api/src/agents/resources.test.ts | 28 +++++++++++++++++++++++ packages/api/src/agents/resources.ts | 22 ++++++++++++------ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/api/src/agents/initialize.ts b/packages/api/src/agents/initialize.ts index 0e967d6f42..0422ecc128 100644 --- a/packages/api/src/agents/initialize.ts +++ b/packages/api/src/agents/initialize.ts @@ -1198,6 +1198,7 @@ export async function initializeAgent( provisionState, warnings: provisionWarnings, } = await primeResources({ + req: params.req, principal: user, getFiles: db.getFiles as never, filterFiles: db.filterFilesByAgentAccess, diff --git a/packages/api/src/agents/resources.test.ts b/packages/api/src/agents/resources.test.ts index 320d5ab30a..7ca2119f4f 100644 --- a/packages/api/src/agents/resources.test.ts +++ b/packages/api/src/agents/resources.test.ts @@ -2004,5 +2004,33 @@ describe('primeResources', () => { expect(result.provisionState).toBeUndefined(); expect(refFile.metadata?.codeEnvRef).toBeDefined(); }); + + it('skips the liveness check when JWT auth has no req to mint from', async () => { + process.env.CODEAPI_AUTH_PROVIDER = 'librechat-jwt'; + const checkSessionsAlive = jest.fn(); + const refFile = makeCodeFile({ + file_id: 'principal-file', + metadata: { + codeEnvRef: { kind: 'user', id: 'user1', storage_session_id: 'sess', file_id: 'remote' }, + }, + }); + + const result = await primeResources({ + principal: { id: 'user1', role: 'USER' }, + appConfig: mockAppConfig, + getFiles: mockGetFiles, + filterFiles: mockFilterFiles, + tool_resources: {}, + attachments: Promise.resolve([refFile]), + requestFileSet, + agentId: 'agent1', + enabledToolResources: new Set([EToolResources.execute_code]), + checkSessionsAlive, + }); + + expect(checkSessionsAlive).not.toHaveBeenCalled(); + expect(result.provisionState).toBeUndefined(); + expect(refFile.metadata?.codeEnvRef).toBeDefined(); + }); }); }); diff --git a/packages/api/src/agents/resources.ts b/packages/api/src/agents/resources.ts index 609fdba844..c04a104071 100644 --- a/packages/api/src/agents/resources.ts +++ b/packages/api/src/agents/resources.ts @@ -3,7 +3,7 @@ import { EModelEndpoint, EToolResources, AgentCapabilities } from 'librechat-dat import type { AgentToolResources, TFile, AgentBaseResource } from 'librechat-data-provider'; import type { IMongoFile, AppConfig, IUser } from '@librechat/data-schemas'; import type { FilterQuery, QueryOptions, ProjectionType } from 'mongoose'; -import type { Request as ServerRequest } from 'express'; +import type { ServerRequest } from '~/types'; import { isCodeApiJwtAuthEnabled } from '~/auth/codeapi'; import { TOOL_RESOURCE_KEYS } from './orphans'; @@ -37,7 +37,7 @@ export type TFileUpdate = { * @returns The codeEnvRef and a deferred DB update object */ export type TProvisionToCodeEnv = (params: { - req: ServerRequest & { user?: IUser }; + req: ServerRequest; file: TFile; entity_id?: string; }) => Promise<{ codeEnvRef: Record; fileUpdate: TFileUpdate }>; @@ -47,7 +47,7 @@ export type TProvisionToCodeEnv = (params: { * @returns Object with embedded status and a deferred DB update object */ export type TProvisionToVectorDB = (params: { - req: ServerRequest & { user?: IUser }; + req: ServerRequest; file: TFile; entity_id?: string; existingStream?: unknown; @@ -60,7 +60,7 @@ export type TProvisionToVectorDB = (params: { */ export type TCheckSessionsAlive = (params: { files: TFile[]; - req?: ServerRequest & { user?: IUser }; + req?: ServerRequest; apiKey?: string; staleSafeWindowMs?: number; }) => Promise>; @@ -239,7 +239,7 @@ export const primeResources = async ({ checkSessionsAlive, loadCodeApiKey, }: { - req?: ServerRequest & { user?: IUser }; + req?: ServerRequest; principal?: Pick; appConfig?: AppConfig; requestFileSet: Set; @@ -467,9 +467,17 @@ export const primeResources = async ({ * LIBRECHAT_CODE_API_KEY is not required for code-env provisioning. */ const codeAuthAvailable = codeApiKey != null || jwtCodeAuth; - // Batch staleness check: identify which code env files are still alive + /** Batch staleness check: identify which code env files are still alive. + * Requires credentials the callback can actually send: a legacy key, or a + * req to mint JWT bearer auth from. Without either, skip the check so an + * unauthorized 401 cannot mark live sandbox files as expired. */ let aliveFileIds: Set | undefined; - if (needsCodeEnv && codeAuthAvailable && checkSessionsAlive) { + if ( + needsCodeEnv && + codeAuthAvailable && + checkSessionsAlive && + (codeApiKey != null || req != null) + ) { const filesWithIdentifiers = attachments.filter( (f) => f?.metadata?.codeEnvRef && f.file_id, );