🧱 fix: Apply the endpoint file policy to persistent agent context files

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.
This commit is contained in:
Danny Avila 2026-08-31 18:30:37 -04:00
parent 25eea4832d
commit 0115f85591
3 changed files with 90 additions and 0 deletions

View file

@ -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[],
});
/**

View file

@ -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 = [];

View file

@ -480,6 +480,7 @@ export const primeResources = async ({
loadCodeApiKey,
provisionCandidates,
legacyFileUploadUX,
filterByEndpointPolicy,
}: {
req?: ServerRequest;
principal?: Pick<IUser, 'id' | 'role'>;
@ -502,6 +503,11 @@ export const primeResources = async ({
provisionCandidates?: Array<TFile>;
/** 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<TFile>) => Array<TFile>;
}): Promise<{
attachments: Array<TFile | undefined> | undefined;
requestAttachments: Array<TFile | undefined> | undefined;
@ -610,6 +616,10 @@ export const primeResources = async ({
agentId,
});
}
if (filterByEndpointPolicy) {
persistedResourceFiles = filterByEndpointPolicy(persistedResourceFiles);
}
}
for (const file of persistedResourceFiles) {