From bc2e9c868f9bd86d121fc72a8fa86d0b4b65b23c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 31 Aug 2026 09:49:03 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20Only=20stream=20provision?= =?UTF-8?q?ing=20sources=20with=20the=20standard=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provisioning called getDownloadStream(req, filepath) for every source, but that contract is not universal: openai takes (file_id, client) and execute_code takes (fileIdentifier, identity, req) and returns an Axios response, so files backed by those sources were mis-invoked rather than provisioned. Streaming now goes through an allowlist of storage-backed sources, so an unfamiliar source is skipped with a warning instead of called incorrectly. --- api/server/services/Files/provision.js | 41 +++++++++++++++++---- api/server/services/Files/provision.spec.js | 20 ++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/api/server/services/Files/provision.js b/api/server/services/Files/provision.js index 6371a54203..f89d6e293b 100644 --- a/api/server/services/Files/provision.js +++ b/api/server/services/Files/provision.js @@ -20,6 +20,34 @@ const { getStrategyFunctions } = require('./strategies'); const axios = createAxiosInstance(); +/* Sources whose `getDownloadStream` takes `(req, filepath)` and returns a readable. + * Others diverge: `openai` takes `(file_id, client)` and `execute_code` takes + * `(fileIdentifier, identity, req)` returning an Axios response, so calling them + * through this contract fails. An allowlist keeps an unfamiliar source skipped + * rather than mis-invoked. */ +const STORAGE_STREAM_SOURCES = new Set([ + FileSources.local, + FileSources.s3, + FileSources.cloudfront, + FileSources.azure_blob, + FileSources.firebase, +]); + +/** Resolves a storage download stream, or null when the source uses a different contract. */ +async function getStorageStream(file, req) { + if (!STORAGE_STREAM_SOURCES.has(file.source)) { + logger.warn( + `[provision] Cannot stream "${file.filename}" (${file.file_id}) from source "${file.source}": unsupported download contract`, + ); + return null; + } + const { getDownloadStream } = getStrategyFunctions(file.source); + if (!getDownloadStream) { + return null; + } + return await getDownloadStream(req, file.filepath); +} + /** Composes code-API auth: legacy X-API-Key when configured, plus JWT bearer when enabled. */ async function buildCodeApiHeaders({ apiKey, req }) { return { @@ -82,16 +110,14 @@ async function loadCodeApiKey(userId) { * Merged pointers plus the deferred DB update */ async function provisionToCodeEnv({ req, file, entity_id }) { - const { getDownloadStream } = getStrategyFunctions(file.source); - if (!getDownloadStream) { + const { handleFileUpload: uploadCodeEnvFile } = getStrategyFunctions(FileSources.execute_code); + const stream = await getStorageStream(file, req); + if (!stream) { throw new Error( `Cannot provision file "${file.filename}" to code env: storage source "${file.source}" does not support download streams`, ); } - const { handleFileUpload: uploadCodeEnvFile } = getStrategyFunctions(FileSources.execute_code); - const stream = await getDownloadStream(req, file.filepath); - const kind = entity_id ? 'agent' : 'user'; const id = entity_id ?? req.user.id; @@ -147,13 +173,12 @@ async function provisionToVectorDB({ req, file, entity_id, existingStream }) { try { let stream = existingStream; if (!stream) { - const { getDownloadStream } = getStrategyFunctions(file.source); - if (!getDownloadStream) { + stream = await getStorageStream(file, req); + if (!stream) { throw new Error( `Cannot provision file "${file.filename}" to vector DB: storage source "${file.source}" does not support download streams`, ); } - stream = await getDownloadStream(req, file.filepath); } // uploadVectors expects a file-like object with a `path` property for fs.createReadStream. diff --git a/api/server/services/Files/provision.spec.js b/api/server/services/Files/provision.spec.js index 4e6aa03d6e..9cecd0f58c 100644 --- a/api/server/services/Files/provision.spec.js +++ b/api/server/services/Files/provision.spec.js @@ -157,6 +157,26 @@ describe('provisionToCodeEnv', () => { expect(result.referenceSet.codeEnvRefs.default.executionProfile).toBe('default'); }); + it('refuses sources whose download contract differs instead of mis-invoking them', async () => { + const uploadCodeEnvFile = jest.fn(); + setupStrategies(uploadCodeEnvFile); + + await expect( + provisionToCodeEnv({ + req: { user: { id: 'u1' } }, + file: { + file_id: 'f-openai', + filename: 'doc.pdf', + type: 'application/pdf', + source: 'openai', + filepath: '/x/doc.pdf', + metadata: {}, + }, + }), + ).rejects.toThrow(/does not support download streams/); + expect(uploadCodeEnvFile).not.toHaveBeenCalled(); + }); + it('keeps filenames untouched when the extension already matches or the file is not an image', async () => { const uploadCodeEnvFile = jest .fn()