From eb2e6bb5e56a08d02561399fa13d0743553df0a3 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:15:42 -0700 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A7=B3=20fix:=20Limit=20Agent=20File?= =?UTF-8?q?=20Inspection=20to=20File=20References?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/middleware/modelBoundContent.spec.ts | 28 +++++++++++++++++++ .../api/src/middleware/modelBoundContent.ts | 23 ++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/api/src/middleware/modelBoundContent.spec.ts b/packages/api/src/middleware/modelBoundContent.spec.ts index 17388cc3a00..dc94d259b52 100644 --- a/packages/api/src/middleware/modelBoundContent.spec.ts +++ b/packages/api/src/middleware/modelBoundContent.spec.ts @@ -1587,6 +1587,34 @@ 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('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..92ca3fd9e9e 100644 --- a/packages/api/src/middleware/modelBoundContent.ts +++ b/packages/api/src/middleware/modelBoundContent.ts @@ -840,6 +840,27 @@ function getHydratedAgentFiles( return files; } +/** + * Restricts opaque-file checks to the runtime fields that can actually carry + * agent file references. Runtime agents also contain initialized tool schemas; + * walking those unrelated schemas just because an attachment was hydrated can + * exhaust the locator traversal budget before any file field is inspected. + */ +function getAgentFileReferenceInput( + agent: AgentContentInput | null | undefined, +): RuntimeAgentFileContainer | undefined { + if (agent == null) { + return undefined; + } + const runtimeAgent = agent as RuntimeAgentFileContainer; + return { + attachments: runtimeAgent.attachments, + requestAttachments: runtimeAgent.requestAttachments, + agentContextAttachments: runtimeAgent.agentContextAttachments, + tool_resources: runtimeAgent.tool_resources, + }; +} + function isFragmentWithinPath(fragment: TextContentFragment, path: JsonPointer): boolean { return fragment.path === path || fragment.path.startsWith(`${path}/`); } @@ -3879,7 +3900,7 @@ function inspectModelBoundContent( } assertInspectableFileInput( input.filters, - omitResolvedCanonicalFileLocators(agent, agentFilesById, { + omitResolvedCanonicalFileLocators(getAgentFileReferenceInput(agent), agentFilesById, { onTraversalFailure: input.onTraversalFailure, messageCount: input.storedMessages?.length ?? 0, }), From fcb1c373c938f4b56bf0ea5bd9734593538809fa Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:31:23 -0700 Subject: [PATCH 2/2] fix: preserve agent opaque scans --- .../src/middleware/modelBoundContent.spec.ts | 40 +++++++++++++++++++ .../api/src/middleware/modelBoundContent.ts | 25 ++---------- packages/api/src/protection/files.ts | 11 ++++- 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/packages/api/src/middleware/modelBoundContent.spec.ts b/packages/api/src/middleware/modelBoundContent.spec.ts index dc94d259b52..1afc7302ade 100644 --- a/packages/api/src/middleware/modelBoundContent.spec.ts +++ b/packages/api/src/middleware/modelBoundContent.spec.ts @@ -1615,6 +1615,46 @@ describe('assertModelBoundContent', () => { ).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 92ca3fd9e9e..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; @@ -840,27 +841,6 @@ function getHydratedAgentFiles( return files; } -/** - * Restricts opaque-file checks to the runtime fields that can actually carry - * agent file references. Runtime agents also contain initialized tool schemas; - * walking those unrelated schemas just because an attachment was hydrated can - * exhaust the locator traversal budget before any file field is inspected. - */ -function getAgentFileReferenceInput( - agent: AgentContentInput | null | undefined, -): RuntimeAgentFileContainer | undefined { - if (agent == null) { - return undefined; - } - const runtimeAgent = agent as RuntimeAgentFileContainer; - return { - attachments: runtimeAgent.attachments, - requestAttachments: runtimeAgent.requestAttachments, - agentContextAttachments: runtimeAgent.agentContextAttachments, - tool_resources: runtimeAgent.tool_resources, - }; -} - function isFragmentWithinPath(fragment: TextContentFragment, path: JsonPointer): boolean { return fragment.path === path || fragment.path.startsWith(`${path}/`); } @@ -3900,7 +3880,8 @@ function inspectModelBoundContent( } assertInspectableFileInput( input.filters, - omitResolvedCanonicalFileLocators(getAgentFileReferenceInput(agent), agentFilesById, { + 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?.({