diff --git a/packages/api/src/agents/initialize.ts b/packages/api/src/agents/initialize.ts index b4f574720e..bb059af31e 100644 --- a/packages/api/src/agents/initialize.ts +++ b/packages/api/src/agents/initialize.ts @@ -1310,6 +1310,12 @@ export async function initializeAgent( loadCodeApiKey: db.loadCodeApiKey, provisionCandidates: deferredProvisionFiles as unknown as TFile[], legacyFileUploadUX, + filterByEndpointPolicy: (files) => + filterFilesByEndpointRuntimeConfig(appConfig, { + files: files as unknown as IMongoFile[], + endpoint: agent.endpoint ?? '', + endpointType: endpointFileType, + }) as unknown as TFile[], }); /** diff --git a/packages/api/src/agents/resources.test.ts b/packages/api/src/agents/resources.test.ts index 9947963f15..247f135cf9 100644 --- a/packages/api/src/agents/resources.test.ts +++ b/packages/api/src/agents/resources.test.ts @@ -97,6 +97,80 @@ describe('primeResources', () => { }); }); + describe('when the endpoint policy rejects a persistent context file', () => { + it('keeps it out of provisioning and out of attachments', async () => { + /* These files are read inside primeResources, so the caller never sees them to + * filter. A provider or policy change since they were attached must still stop + * their bytes reaching the Code API or RAG. */ + const rejected: TFile[] = [ + { + user: 'user1', + file_id: 'stale-context-file', + filename: 'legacy.csv', + filepath: '/uploads/legacy.csv', + object: 'file' as const, + type: 'text/csv', + bytes: 1024, + embedded: false, + usage: 0, + source: FileSources.local, + }, + ]; + mockGetFiles.mockResolvedValue(rejected); + + const result = await primeResources({ + req: mockReq, + appConfig: mockAppConfig, + getFiles: mockGetFiles, + filterFiles: mockFilterFiles, + requestFileSet, + attachments: undefined, + tool_resources: { [EToolResources.context]: { file_ids: ['stale-context-file'] } }, + agentId: 'agent_test', + enabledToolResources: new Set([EToolResources.execute_code, EToolResources.file_search]), + filterByEndpointPolicy: () => [], + }); + + expect(result.provisionState).toBeUndefined(); + expect(result.attachments).toBeUndefined(); + }); + + it('still provisions a persistent context file the policy allows', async () => { + const allowed: TFile[] = [ + { + user: 'user1', + file_id: 'live-context-file', + filename: 'data.csv', + filepath: '/uploads/data.csv', + object: 'file' as const, + type: 'text/csv', + bytes: 1024, + embedded: false, + usage: 0, + source: FileSources.local, + }, + ]; + mockGetFiles.mockResolvedValue(allowed); + + const result = await primeResources({ + req: mockReq, + appConfig: mockAppConfig, + getFiles: mockGetFiles, + filterFiles: mockFilterFiles, + requestFileSet, + attachments: undefined, + tool_resources: { [EToolResources.context]: { file_ids: ['live-context-file'] } }, + agentId: 'agent_test', + enabledToolResources: new Set([EToolResources.execute_code, EToolResources.file_search]), + filterByEndpointPolicy: (files) => files, + }); + + expect(result.provisionState?.codeEnvFiles.map((f) => f.file_id)).toEqual([ + 'live-context-file', + ]); + }); + }); + describe('when `context` capability is disabled', () => { it('should not fetch context files even if tool_resources has context file_ids', async () => { (mockAppConfig.endpoints![EModelEndpoint.agents] as TAgentsEndpoint).capabilities = []; diff --git a/packages/api/src/agents/resources.ts b/packages/api/src/agents/resources.ts index e2d6c650b2..c62543fbd4 100644 --- a/packages/api/src/agents/resources.ts +++ b/packages/api/src/agents/resources.ts @@ -480,6 +480,7 @@ export const primeResources = async ({ loadCodeApiKey, provisionCandidates, legacyFileUploadUX, + filterByEndpointPolicy, }: { req?: ServerRequest; principal?: Pick; @@ -502,6 +503,11 @@ export const primeResources = async ({ provisionCandidates?: Array; /** True when this endpoint still shows the explicit upload-destination chooser. */ legacyFileUploadUX?: boolean; + /** Applies the current endpoint's file policy. Persistent agent files are read here + * rather than by the caller, so the caller has no chance to filter them itself and + * a provider or policy change since they were attached would otherwise let their + * bytes reach the Code API or RAG. */ + filterByEndpointPolicy?: (files: Array) => Array; }): Promise<{ attachments: Array | undefined; requestAttachments: Array | undefined; @@ -610,6 +616,10 @@ export const primeResources = async ({ agentId, }); } + + if (filterByEndpointPolicy) { + persistedResourceFiles = filterByEndpointPolicy(persistedResourceFiles); + } } for (const file of persistedResourceFiles) {