Skip to content

Commit 4efb0ec

Browse files
fix(workflows): scope executor metadata reads
1 parent 9f17b8c commit 4efb0ec

2 files changed

Lines changed: 125 additions & 4 deletions

File tree

apps/sim/providers/utils.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing'
22
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
const workflowMetadataMocks = vi.hoisted(() => ({
5+
buildAPIUrl: vi.fn((path: string) => new URL(path, 'https://sim.local')),
6+
buildExecutorDelegationHeaders: vi.fn(),
7+
}))
8+
9+
vi.mock('@/executor/utils/http', () => ({
10+
buildAPIUrl: workflowMetadataMocks.buildAPIUrl,
11+
buildExecutorDelegationHeaders: workflowMetadataMocks.buildExecutorDelegationHeaders,
12+
}))
13+
314
import {
415
calculateCost,
516
describeModelLevel,
@@ -1841,6 +1852,105 @@ describe('prepareToolExecution invoker identity hand-off', () => {
18411852
})
18421853
})
18431854

1855+
describe('workflow executor metadata delegation', () => {
1856+
const workflowBlock = {
1857+
type: 'workflow',
1858+
name: 'Workflow',
1859+
description: 'Execute a workflow',
1860+
inputs: {},
1861+
subBlocks: [],
1862+
tools: { access: ['workflow_executor'] },
1863+
}
1864+
const workflowTool = {
1865+
id: 'workflow_executor',
1866+
name: 'Workflow Executor',
1867+
description: 'Execute another workflow',
1868+
params: {
1869+
workflowId: {
1870+
type: 'string' as const,
1871+
required: true,
1872+
visibility: 'user-only' as const,
1873+
},
1874+
},
1875+
}
1876+
1877+
beforeEach(() => {
1878+
vi.clearAllMocks()
1879+
workflowMetadataMocks.buildExecutorDelegationHeaders.mockResolvedValue({
1880+
'Content-Type': 'application/json',
1881+
Authorization: 'Bearer delegated-token',
1882+
})
1883+
})
1884+
1885+
afterEach(() => {
1886+
vi.unstubAllGlobals()
1887+
})
1888+
1889+
it('binds workflow metadata reads to the target workflow and trusted execution subject', async () => {
1890+
const fetchMock = vi
1891+
.fn()
1892+
.mockResolvedValue(
1893+
new Response(
1894+
JSON.stringify({ data: { name: 'Child Workflow', description: 'Child description' } }),
1895+
{ status: 200, headers: { 'Content-Type': 'application/json' } }
1896+
)
1897+
)
1898+
vi.stubGlobal('fetch', fetchMock)
1899+
1900+
const result = await transformBlockTool(
1901+
{ type: 'workflow', params: { workflowId: 'child-workflow' } },
1902+
{
1903+
getAllBlocks: () => [workflowBlock],
1904+
getTool: () => workflowTool,
1905+
enrichmentContext: {
1906+
workflowId: 'parent-workflow',
1907+
workspaceId: 'workspace-1',
1908+
executionId: 'execution-1',
1909+
userId: 'user-1',
1910+
},
1911+
}
1912+
)
1913+
1914+
expect(workflowMetadataMocks.buildExecutorDelegationHeaders).toHaveBeenCalledWith({
1915+
subjectUserId: 'user-1',
1916+
workflowId: 'child-workflow',
1917+
executionId: 'execution-1',
1918+
})
1919+
expect(fetchMock).toHaveBeenCalledWith('https://sim.local/api/workflows/child-workflow', {
1920+
headers: {
1921+
'Content-Type': 'application/json',
1922+
Authorization: 'Bearer delegated-token',
1923+
},
1924+
})
1925+
expect(result).toMatchObject({
1926+
id: 'workflow_executor_child-workflow',
1927+
name: 'Child Workflow',
1928+
description: 'Child description',
1929+
})
1930+
})
1931+
1932+
it('does not issue an actorless fallback token without a trusted execution subject', async () => {
1933+
const fetchMock = vi.fn()
1934+
vi.stubGlobal('fetch', fetchMock)
1935+
1936+
const result = await transformBlockTool(
1937+
{ type: 'workflow', params: { workflowId: 'child-workflow' } },
1938+
{
1939+
getAllBlocks: () => [workflowBlock],
1940+
getTool: () => workflowTool,
1941+
}
1942+
)
1943+
1944+
expect(workflowMetadataMocks.buildExecutorDelegationHeaders).not.toHaveBeenCalled()
1945+
expect(fetchMock).not.toHaveBeenCalled()
1946+
expect(result).toMatchObject({
1947+
id: 'workflow_executor_child-workflow',
1948+
name: 'Workflow Executor',
1949+
description: 'Execute another workflow',
1950+
})
1951+
})
1952+
})
1953+
18441954
/**
18451955
* The agent block's tuning-level fields accept variable and environment references, so any
18461956
* message that echoes a caller-supplied level can otherwise carry whatever that reference

apps/sim/providers/utils.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,20 @@ function isDefaultWorkflowDescription(
7777
* Fetches workflow metadata (name and description) from the API
7878
*/
7979
async function fetchWorkflowMetadata(
80-
workflowId: string
80+
workflowId: string,
81+
executionContext: WorkflowToolExecutionContext | undefined
8182
): Promise<{ name: string; description: string | null } | null> {
8283
try {
83-
const { buildAuthHeaders, buildAPIUrl } = await import('@/executor/utils/http')
84+
if (!executionContext?.userId) {
85+
throw new Error('Workflow metadata enrichment requires a trusted execution subject')
86+
}
87+
const { buildAPIUrl, buildExecutorDelegationHeaders } = await import('@/executor/utils/http')
8488

85-
const headers = await buildAuthHeaders()
89+
const headers = await buildExecutorDelegationHeaders({
90+
subjectUserId: executionContext.userId,
91+
workflowId,
92+
...(executionContext.executionId ? { executionId: executionContext.executionId } : {}),
93+
})
8694
const url = buildAPIUrl(`/api/workflows/${workflowId}`)
8795

8896
const response = await fetch(url.toString(), { headers })
@@ -787,7 +795,10 @@ export async function transformBlockTool(
787795
if (toolId === 'workflow_executor' && resolvedResourceParams.workflowId) {
788796
uniqueToolId = `${toolConfig.id}_${resolvedResourceParams.workflowId}`
789797

790-
const workflowMetadata = await fetchWorkflowMetadata(resolvedResourceParams.workflowId)
798+
const workflowMetadata = await fetchWorkflowMetadata(
799+
resolvedResourceParams.workflowId,
800+
enrichmentContext
801+
)
791802
if (workflowMetadata) {
792803
toolName = workflowMetadata.name || toolConfig.name
793804
if (

0 commit comments

Comments
 (0)