From 0115f85591791554ac451f8c2148a3dec726d775 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 31 Aug 2026 18:30:37 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B1=20fix:=20Apply=20the=20endpoint=20?= =?UTF-8?q?file=20policy=20to=20persistent=20agent=20context=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A saved agent's persistent context files are read inside primeResources, after the caller has already applied the endpoint policy to the request's own files and to the deferred provisioning candidates. Nothing filtered them, so once the agent's provider or file configuration changed, a file the current endpoint disables or refuses by size or MIME type could still be queued and sent to the Code API or RAG on the next tool call. The caller now passes its endpoint policy down, since it owns the endpoint resolution and primeResources owns the read. Both the no-attachment turn and the turn that also carries new attachments go through it. --- packages/api/src/agents/initialize.ts | 6 ++ packages/api/src/agents/resources.test.ts | 74 +++++++++++++++++++++++ packages/api/src/agents/resources.ts | 10 +++ 3 files changed, 90 insertions(+) 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) {