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()