diff --git a/packages/api/src/middleware/modelBoundContent.spec.ts b/packages/api/src/middleware/modelBoundContent.spec.ts index 17388cc3a00..1afc7302ade 100644 --- a/packages/api/src/middleware/modelBoundContent.spec.ts +++ b/packages/api/src/middleware/modelBoundContent.spec.ts @@ -1587,6 +1587,74 @@ describe('assertModelBoundContent', () => { ).toThrow('Submitted content contains a private context'); }); + it('does not traverse unrelated initialized tool schemas while checking agent files', () => { + const hydratedFile = { + file_id: 'file-agent-context', + filename: 'context.txt', + filepath: '/uploads/context.txt', + text: 'safe canonical context', + }; + + expect(() => + assertModelBoundContent({ + filters, + agents: [ + { + instructions: 'Safe agent instructions', + toolDefinitions: [ + { + name: 'deep_tool', + description: 'Safe tool description', + parameters: makeDeepModelParameter(), + }, + ], + requestAttachments: [hydratedFile], + } as never, + ], + }), + ).not.toThrow(); + }); + + it('still checks provider-bound agent fields for opaque file content', () => { + const failClosedFilters: FiltersConfig = { + files: { + pii: { + fields: ['content'], + starterPatterns: [], + uninspectable: 'block', + }, + }, + }; + const hydratedFile = { + file_id: 'file-agent-context', + filename: 'context.txt', + filepath: '/uploads/context.txt', + content: 'safe canonical context', + }; + + for (const agent of [ + { + instructions: 'data:application/pdf;base64,opaque', + requestAttachments: [hydratedFile], + }, + { + model_parameters: { + additionalModelRequestFields: { + input_file: { file_data: 'data:application/pdf;base64,opaque' }, + }, + }, + requestAttachments: [hydratedFile], + }, + ]) { + expect(() => + assertModelBoundContent({ + filters: failClosedFilters, + agents: [agent as never], + }), + ).toThrow('Submitted file content could not be inspected before processing.'); + } + }); + it('accepts only agent resource IDs backed by inspectable hydrated resource files', () => { const filters: FiltersConfig = { files: { diff --git a/packages/api/src/middleware/modelBoundContent.ts b/packages/api/src/middleware/modelBoundContent.ts index b89d5101661..c7ecb458806 100644 --- a/packages/api/src/middleware/modelBoundContent.ts +++ b/packages/api/src/middleware/modelBoundContent.ts @@ -147,6 +147,7 @@ const MAX_PROVIDER_PROVENANCE_PARSE_WORK = MAX_PROVIDER_PROVENANCE_INDEX_REFS + MAX_PROVIDER_PROVENANCE_PARTS; const MAX_PROVIDER_STORED_STATE_WORK = MAX_PROVIDER_PROJECTION_WORK + MAX_PROVIDER_PROVENANCE_PARTS * 2; +const AGENT_FILE_INSPECTION_OMITTED_ROOT_KEYS = new Set(['toolDefinitions']); /** One root plus bounded structural bookkeeping for every valid provider part. */ const MAX_MODEL_BOUND_NESTED_TRAVERSAL_WORK = CONTENT_TRAVERSAL_MAX_NODES * 2; @@ -3880,6 +3881,7 @@ function inspectModelBoundContent( assertInspectableFileInput( input.filters, omitResolvedCanonicalFileLocators(agent, agentFilesById, { + omittedRootKeys: AGENT_FILE_INSPECTION_OMITTED_ROOT_KEYS, onTraversalFailure: input.onTraversalFailure, messageCount: input.storedMessages?.length ?? 0, }), diff --git a/packages/api/src/protection/files.ts b/packages/api/src/protection/files.ts index e406177be5c..49352ef9ac7 100644 --- a/packages/api/src/protection/files.ts +++ b/packages/api/src/protection/files.ts @@ -1029,6 +1029,7 @@ function omitResolvedFileLocators( depth = 0, state?: { readonly seen: WeakMap; + readonly omittedRootKeys?: ReadonlySet; visited: number; }, ): unknown { @@ -1098,6 +1099,9 @@ function omitResolvedFileLocators( }; for (const [key, child] of entries) { + if (depth === 0 && traversal.omittedRootKeys?.has(key)) { + continue; + } if (resolvedFile != null && key === 'file_id') { continue; } @@ -1143,6 +1147,7 @@ export function omitResolvedCanonicalFileLocators( resolvedFilesById: ReadonlyMap, context: { readonly messageCount?: number; + readonly omittedRootKeys?: ReadonlySet; readonly onTraversalFailure?: LocatorTraversalReporter; } = {}, ): T { @@ -1150,7 +1155,11 @@ export function omitResolvedCanonicalFileLocators( return input; } try { - return omitResolvedFileLocators(input, resolvedFilesById) as T; + return omitResolvedFileLocators(input, resolvedFilesById, 0, { + seen: new WeakMap(), + omittedRootKeys: context.omittedRootKeys, + visited: 0, + }) as T; } catch (error) { if (error instanceof ContentTraversalLimitError && error.diagnostics != null) { context.onTraversalFailure?.({