Skip to content

Commit 738e955

Browse files
committed
test(function): prove direct tool calls against the real function-execution policy
1 parent e7dad08 commit 738e955

2 files changed

Lines changed: 70 additions & 59 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { createPersonalApiKeyPrincipal } from '@sim/testing/factories/principal.factory'
2+
import { workspaceAuthzMock, workspaceAuthzMockFns } from '@sim/testing/mocks/workspace-authz.mock'
3+
import {
4+
workspaceContextMock,
5+
workspaceContextMockFns,
6+
} from '@sim/testing/mocks/workspace-context.mock'
7+
import { beforeEach, describe, expect, it, vi } from 'vitest'
8+
9+
const mocks = vi.hoisted(() => ({ runSandbox: vi.fn() }))
10+
11+
vi.mock('@/lib/function-execution/execute-request', () => ({
12+
executeFunctionRequest: mocks.runSandbox,
13+
}))
14+
vi.mock('@/lib/workspaces/application/workspace-context', () => workspaceContextMock)
15+
vi.mock('@sim/platform-authz/workspace', () => workspaceAuthzMock)
16+
17+
import type { Principal } from '@sim/auth/principal'
18+
import { markCopilotWorkspaceInvocation } from '@/lib/core/application/copilot-workspace-invocation'
19+
import { PrincipalKindAuthorizationError } from '@/lib/core/application/workspace-authorization'
20+
import { executeFunctionTool } from '@/lib/internal/function/execute'
21+
import { createCopilotChatPrincipal } from '@/lib/mothership/auth/application-delegation'
22+
import { TOOL_EXECUTION_DELEGATION_AUDIENCE } from '@/lib/tool-execution/application/operations'
23+
24+
/**
25+
* `POST /api/v2/tools/{id}/execute` (the embedded CLI's `tools execute`) runs `function_execute`
26+
* with no workflow behind it, so no executor origin exists. The real function-execution policy
27+
* decides from the authenticated caller; only the sandbox is stubbed.
28+
*/
29+
function runDirect(callerPrincipal: Principal) {
30+
return executeFunctionTool({
31+
body: { code: 'return 1' },
32+
headers: new Headers(),
33+
requestId: 'request-1',
34+
context: { workflowId: '', workspaceId: 'workspace-1', userId: 'user-1', callerPrincipal },
35+
})
36+
}
37+
38+
describe('executeFunctionTool direct tool calls', () => {
39+
beforeEach(() => {
40+
workspaceContextMockFns.mockResolveActiveWorkspaceApplicationContext.mockResolvedValue({
41+
workspaceId: 'workspace-1',
42+
workspaceOrganizationId: null,
43+
billedAccountUserId: 'user-1',
44+
allowPersonalApiKeys: true,
45+
})
46+
workspaceAuthzMockFns.mockResolveEffectiveWorkspacePermission.mockResolvedValue('write')
47+
mocks.runSandbox.mockResolvedValue(Response.json({ success: true, output: { result: 1 } }))
48+
})
49+
50+
it('runs the code for an admitted Mothership caller', async () => {
51+
const caller = createCopilotChatPrincipal(
52+
{ userId: 'user-1', workspaceId: 'workspace-1', chatId: 'chat-1' },
53+
TOOL_EXECUTION_DELEGATION_AUDIENCE
54+
)
55+
markCopilotWorkspaceInvocation(caller)
56+
57+
const response = await runDirect(caller)
58+
59+
expect(response.status).toBe(200)
60+
expect(await response.json()).toEqual({ success: true, output: { result: 1 } })
61+
})
62+
63+
it('still refuses a personal API key before any code runs', async () => {
64+
await expect(runDirect(createPersonalApiKeyPrincipal({ keyId: 'key-1' }))).rejects.toThrow(
65+
PrincipalKindAuthorizationError
66+
)
67+
expect(mocks.runSandbox).not.toHaveBeenCalled()
68+
})
69+
})

‎apps/sim/lib/internal/function/execute.test.ts‎

Lines changed: 1 addition & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { Principal } from '@sim/auth/principal'
2-
import { createPersonalApiKeyPrincipal } from '@sim/testing/factories/principal.factory'
31
import {
42
executorPrincipalMock,
53
executorPrincipalMockFns,
@@ -14,18 +12,15 @@ const mocks = vi.hoisted(() => ({
1412
vi.mock('@/lib/internal/principals/executor', () => executorPrincipalMock)
1513

1614
vi.mock('@/lib/function-execution/application/execute-function', () => ({
17-
executeFunction: { execute: mocks.execute, delegationAudience: 'sim:function-executions' },
15+
executeFunction: { execute: mocks.execute },
1816
}))
1917

2018
vi.mock('@/lib/function-execution/application/execute-chat-function', () => ({
2119
executeChatFunction: { execute: mocks.executeChat },
2220
}))
2321

24-
import { markCopilotWorkspaceInvocation } from '@/lib/core/application/copilot-workspace-invocation'
2522
import { FUNCTION_EXECUTION_DELEGATION_AUDIENCE } from '@/lib/function-execution/application/authorization'
2623
import { executeFunctionTool } from '@/lib/internal/function/execute'
27-
import { createCopilotChatPrincipal } from '@/lib/mothership/auth/application-delegation'
28-
import { TOOL_EXECUTION_DELEGATION_AUDIENCE } from '@/lib/tool-execution/application/operations'
2924
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
3025

3126
const { mockCreateExecutorPrincipalFromExecutionContext } = executorPrincipalMockFns
@@ -174,57 +169,4 @@ describe('executeFunctionTool', () => {
174169
expect(mocks.executeChat).not.toHaveBeenCalled()
175170
expect(mocks.execute).not.toHaveBeenCalled()
176171
})
177-
178-
describe('direct tool calls (POST /api/v2/tools/{id}/execute)', () => {
179-
function directContext(callerPrincipal: Principal) {
180-
return {
181-
workflowId: '',
182-
workspaceId: 'workspace-1',
183-
userId: 'user-1',
184-
callerPrincipal,
185-
}
186-
}
187-
188-
it('rebinds an admitted Mothership caller to the function-execution audience', async () => {
189-
const caller = createCopilotChatPrincipal(
190-
{ userId: 'user-1', workspaceId: 'workspace-1', chatId: 'chat-1' },
191-
TOOL_EXECUTION_DELEGATION_AUDIENCE
192-
)
193-
markCopilotWorkspaceInvocation(caller)
194-
195-
await executeFunctionTool({
196-
body: { code: 'return 1' },
197-
headers: new Headers(),
198-
requestId: 'request-1',
199-
context: directContext(caller),
200-
})
201-
202-
expect(mockCreateExecutorPrincipalFromExecutionContext).not.toHaveBeenCalled()
203-
expect(mocks.execute).toHaveBeenCalledWith(
204-
expect.objectContaining({
205-
principal: expect.objectContaining({
206-
kind: 'delegated',
207-
serviceId: 'copilot',
208-
subjectUserId: 'user-1',
209-
workspaceId: 'workspace-1',
210-
audience: FUNCTION_EXECUTION_DELEGATION_AUDIENCE,
211-
}),
212-
})
213-
)
214-
})
215-
216-
it('hands any other caller to the function-execution policy unchanged', async () => {
217-
const caller = createPersonalApiKeyPrincipal({ keyId: 'personal-key-1' })
218-
219-
await executeFunctionTool({
220-
body: { code: 'return 1' },
221-
headers: new Headers(),
222-
requestId: 'request-1',
223-
context: directContext(caller),
224-
})
225-
226-
expect(mockCreateExecutorPrincipalFromExecutionContext).not.toHaveBeenCalled()
227-
expect(mocks.execute).toHaveBeenCalledWith(expect.objectContaining({ principal: caller }))
228-
})
229-
})
230172
})

0 commit comments

Comments
 (0)