Skip to content

Commit f21bf92

Browse files
fix(agent): authorize workspace attachments through execution delegation (#7859)
* fix(agent): authorize workspace attachments through execution delegation * test(agent): preserve rejection of unprefixed attachment keys
1 parent 585e8f7 commit f21bf92

10 files changed

Lines changed: 703 additions & 54 deletions

apps/sim/executor/handlers/agent/agent-handler.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,52 @@ describe('AgentBlockHandler', () => {
953953
expect(inputs).toEqual(rawInputs)
954954
})
955955

956+
it.each([
957+
'url/https://example.com/image.png',
958+
'',
959+
'provider-file-id',
960+
'profile-pictures/avatar.png',
961+
])('preserves inline bytes for an actorless request with key %s', async (key) => {
962+
mockGetProviderFromModel.mockReturnValue('openai')
963+
await handler.execute(
964+
{
965+
...mockContext,
966+
principal: {
967+
kind: 'system',
968+
serviceId: 'chat',
969+
workspaceId: 'test-workspace',
970+
workflowId: 'test-workflow',
971+
},
972+
executorDelegationOrigin: undefined,
973+
},
974+
mockBlock,
975+
{
976+
model: 'gpt-4o',
977+
messages: [
978+
{
979+
role: 'user',
980+
content: 'Analyze this image',
981+
files: [
982+
{
983+
id: 'file-1',
984+
key,
985+
name: 'image.png',
986+
url: 'https://example.com/image.png',
987+
size: 5,
988+
type: 'image/png',
989+
base64: 'aW1hZ2U=',
990+
},
991+
],
992+
},
993+
],
994+
apiKey: 'test-api-key',
995+
}
996+
)
997+
expect(mockExecuteProviderRequest.mock.calls[0][1].messages[0].files).toEqual([
998+
expect.objectContaining({ base64: 'aW1hZ2U=' }),
999+
])
1000+
})
1001+
9561002
it('normalizes the persisted workspace-picker shape before provider execution', async () => {
9571003
const key = 'workspace/ws-1/example.png'
9581004
const hydrationSpy = vi

apps/sim/executor/handlers/agent/agent-handler.ts

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
selectModelSchemaInputPaths,
1010
} from '@/lib/execution/model-input-provenance'
1111
import { readAvailableCustomToolByIdOrTitleAsExecutor } from '@/lib/internal/custom-tools/read-available-by-id-or-title'
12+
import { resolveExecutorFileMaterializationContext } from '@/lib/internal/file/materialization-context'
1213
import { discoverMcpServerToolsAsExecutor } from '@/lib/internal/mcp/discover-tools'
1314
import {
1415
readWorkflowInputFieldsForTool,
@@ -31,6 +32,7 @@ import {
3132
MODEL_SUPPORTED_IMAGE_MIME_TYPES,
3233
processFilesToUserFiles,
3334
type RawFileInput,
35+
tryInferContextFromKey,
3436
} from '@/lib/uploads/utils/file-utils'
3537
import { selectModelBoundFileInputPaths } from '@/lib/uploads/utils/model-input'
3638
import { hydrateUserFilesWithBase64 } from '@/lib/uploads/utils/user-file-base64.server'
@@ -63,7 +65,7 @@ import type {
6365
ToolInput,
6466
} from '@/executor/handlers/agent/types'
6567
import { parseResponseFormat } from '@/executor/handlers/shared/response-format'
66-
import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/executor/types'
68+
import type { BlockHandler, ExecutionContext, StreamingExecution, UserFile } from '@/executor/types'
6769
import { collectBlockData } from '@/executor/utils/block-data'
6870
import { stringifyJSON } from '@/executor/utils/json'
6971
import { projectResolvedSecretDiagnosticContent } from '@/executor/utils/resolved-secret-content-projection'
@@ -1481,7 +1483,6 @@ export class AgentBlockHandler implements BlockHandler {
14811483
throw new Error(`File attachments are not supported for provider "${providerId}"`)
14821484
}
14831485

1484-
const requestId = ctx.executionId || ctx.workflowId || 'agent-files'
14851486
const nextMessages = [...messages]
14861487

14871488
const inlineMaxBytes = getInlineHydrationMaxBytes(providerId)
@@ -1493,36 +1494,46 @@ export class AgentBlockHandler implements BlockHandler {
14931494
}
14941495

14951496
const unsafeGeneratedDocumentFiles = new Set<string>()
1496-
const hydratedFiles = await hydrateUserFilesWithBase64(message.files, {
1497-
requestId,
1498-
workspaceId: ctx.workspaceId,
1499-
workflowId: ctx.workflowId,
1500-
executionId: ctx.executionId,
1501-
largeValueExecutionIds: ctx.largeValueExecutionIds,
1502-
largeValueKeys: ctx.largeValueKeys,
1503-
fileKeys: ctx.fileKeys,
1504-
allowLargeValueWorkflowScope: ctx.allowLargeValueWorkflowScope,
1505-
userId: ctx.userId,
1506-
principal: ctx.principal,
1507-
logger,
1508-
maxBytes: inlineMaxBytes,
1509-
onServableFileContributors: async (file, contributors) => {
1510-
if (!ctx.workspaceId) return
1511-
for (const identity of contributors) {
1512-
const safe = await importWorkspaceFileSecretProvenanceForModelView({
1513-
workspaceId: ctx.workspaceId,
1514-
identity,
1515-
registry: ctx.resolvedSecretTraceRegistry,
1516-
view: 'opaque',
1517-
...(ctx.userId ? { actorUserId: ctx.userId } : {}),
1518-
})
1519-
if (!safe) {
1520-
unsafeGeneratedDocumentFiles.add(`${file.key}:${file.id}`)
1521-
return
1522-
}
1523-
}
1524-
},
1497+
const groups = new Map<boolean, Array<{ file: UserFile; index: number }>>()
1498+
message.files.forEach((file, index) => {
1499+
const workspaceFile =
1500+
ctx.principal?.kind === 'system' && tryInferContextFromKey(file.key) === 'workspace'
1501+
const group = groups.get(workspaceFile) ?? []
1502+
group.push({ file, index })
1503+
groups.set(workspaceFile, group)
15251504
})
1505+
const hydratedFiles = [...message.files]
1506+
await Promise.all(
1507+
[...groups.values()].map(async (group) => {
1508+
const hydrated = await hydrateUserFilesWithBase64(
1509+
group.map(({ file }) => file),
1510+
{
1511+
...(await resolveExecutorFileMaterializationContext(ctx, group[0].file)),
1512+
logger,
1513+
maxBytes: inlineMaxBytes,
1514+
onServableFileContributors: async (file, contributors) => {
1515+
if (!ctx.workspaceId) return
1516+
for (const identity of contributors) {
1517+
const safe = await importWorkspaceFileSecretProvenanceForModelView({
1518+
workspaceId: ctx.workspaceId,
1519+
identity,
1520+
registry: ctx.resolvedSecretTraceRegistry,
1521+
view: 'opaque',
1522+
...(ctx.userId ? { actorUserId: ctx.userId } : {}),
1523+
})
1524+
if (!safe) {
1525+
unsafeGeneratedDocumentFiles.add(`${file.key}:${file.id}`)
1526+
return
1527+
}
1528+
}
1529+
},
1530+
}
1531+
)
1532+
group.forEach(({ index }, fileIndex) => {
1533+
hydratedFiles[index] = hydrated[fileIndex]
1534+
})
1535+
})
1536+
)
15261537

15271538
const modelSafeHydratedFiles = hydratedFiles.flatMap((file, fileIndex) => {
15281539
if (unsafeGeneratedDocumentFiles.has(`${file.key}:${file.id}`)) return []

0 commit comments

Comments
 (0)