Skip to content

Commit 9d7ab61

Browse files
committed
Tighten Composio tool validation
1 parent 6f39d15 commit 9d7ab61

7 files changed

Lines changed: 79 additions & 51 deletions

File tree

common/src/constants/composio.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,11 @@ export const COMPOSIO_META_TOOL_NAMES = [
88
] as const
99

1010
export type ComposioMetaToolName = (typeof COMPOSIO_META_TOOL_NAMES)[number]
11+
12+
const COMPOSIO_META_TOOL_NAME_SET = new Set<string>(COMPOSIO_META_TOOL_NAMES)
13+
14+
export function isComposioMetaToolName(
15+
toolName: string,
16+
): toolName is ComposioMetaToolName {
17+
return COMPOSIO_META_TOOL_NAME_SET.has(toolName)
18+
}

sdk/src/__tests__/composio.test.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { afterEach, describe, expect, mock, test } from 'bun:test'
33
import { COMPOSIO_META_TOOL_NAMES } from '@codebuff/common/constants/composio'
44
import { clientToolNames, toolParams } from '@codebuff/common/tools/list'
55

6-
import {
7-
executeComposioToolViaServer,
8-
normalizeComposioInput,
9-
} from '../composio'
6+
import { executeComposioToolViaServer } from '../composio'
107

118
describe('Composio SDK tools', () => {
129
const originalFetch = globalThis.fetch
@@ -64,7 +61,27 @@ describe('Composio SDK tools', () => {
6461
expect(fetchMock).toHaveBeenCalledTimes(1)
6562
})
6663

67-
test('normalizes non-object Composio inputs for server execution', () => {
68-
expect(normalizeComposioInput('gmail')).toEqual({ value: 'gmail' })
64+
test('returns a tool error when the server response is malformed', async () => {
65+
globalThis.fetch = mock(
66+
async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
67+
) as unknown as typeof fetch
68+
69+
const output = await executeComposioToolViaServer({
70+
apiKey: 'codebuff-api-key',
71+
toolName: 'COMPOSIO_SEARCH_TOOLS',
72+
input: {
73+
queries: ['find gmail tools'],
74+
session: { generate_id: true },
75+
},
76+
})
77+
78+
expect(output).toEqual([
79+
{
80+
type: 'json',
81+
value: {
82+
errorMessage: 'Invalid Composio execute response from server',
83+
},
84+
},
85+
])
6986
})
7087
})

sdk/src/composio.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
11
import { WEBSITE_URL } from './constants'
22

33
import type { ComposioMetaToolName } from '@codebuff/common/constants/composio'
4-
import type { JSONValue } from '@codebuff/common/types/json'
54
import type { ToolResultOutput } from '@codebuff/common/types/messages/content-part'
65

76
type ComposioExecuteResponse = {
87
output: ToolResultOutput[]
98
}
109

11-
function toJsonValue(value: unknown): JSONValue {
12-
try {
13-
return JSON.parse(JSON.stringify(value ?? null)) as JSONValue
14-
} catch {
15-
return String(value) as JSONValue
16-
}
17-
}
18-
1910
async function readErrorMessage(response: Response): Promise<string> {
2011
try {
2112
const body = (await response.json()) as {
@@ -62,6 +53,16 @@ export async function executeComposioToolViaServer(params: {
6253
}
6354

6455
const body = (await response.json()) as ComposioExecuteResponse
56+
if (!Array.isArray(body.output)) {
57+
return [
58+
{
59+
type: 'json',
60+
value: {
61+
errorMessage: 'Invalid Composio execute response from server',
62+
},
63+
},
64+
]
65+
}
6566
return body.output
6667
} catch (error) {
6768
return [
@@ -74,11 +75,3 @@ export async function executeComposioToolViaServer(params: {
7475
]
7576
}
7677
}
77-
78-
export function normalizeComposioInput(
79-
input: unknown,
80-
): Record<string, unknown> {
81-
return input && typeof input === 'object'
82-
? (input as Record<string, unknown>)
83-
: { value: toJsonValue(input) }
84-
}

sdk/src/run.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,15 @@ import {
1414
} from '@codebuff/common/mcp/client'
1515
import {
1616
COMPOSIO_META_TOOL_NAMES,
17-
type ComposioMetaToolName,
17+
isComposioMetaToolName,
1818
} from '@codebuff/common/constants/composio'
1919
import { toolNames } from '@codebuff/common/tools/constants'
2020
import { clientToolCallSchema } from '@codebuff/common/tools/list'
2121
import { AgentOutputSchema } from '@codebuff/common/types/session-state'
2222
import { extractApiErrorDetails } from '@codebuff/common/util/error'
2323
import { cloneDeep } from 'lodash'
2424

25-
import {
26-
executeComposioToolViaServer,
27-
normalizeComposioInput,
28-
} from './composio'
25+
import { executeComposioToolViaServer } from './composio'
2926
import { getErrorStatusCode } from './error-utils'
3027
import { getAgentRuntimeImpl } from './impl/agent-runtime'
3128
import { getUserInfoFromApiKey } from './impl/database'
@@ -740,13 +737,11 @@ async function handleToolCall({
740737
},
741738
},
742739
]
743-
} else if (
744-
COMPOSIO_META_TOOL_NAMES.includes(toolName as ComposioMetaToolName)
745-
) {
740+
} else if (isComposioMetaToolName(toolName)) {
746741
result = await executeComposioToolViaServer({
747742
apiKey,
748-
toolName: toolName as ComposioMetaToolName,
749-
input: normalizeComposioInput(input),
743+
toolName,
744+
input,
750745
})
751746
} else {
752747
throw new Error(

web/src/app/api/v1/composio/__tests__/composio.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,34 @@ describe('/api/v1/composio', () => {
169169
expect(executeTool).not.toHaveBeenCalled()
170170
})
171171

172+
test('rejects unsupported Composio tool names before execution', async () => {
173+
const executeTool = mock(async () => [
174+
{ type: 'json' as const, value: { ok: true } },
175+
])
176+
const req = new NextRequest('http://localhost/api/v1/composio/execute', {
177+
method: 'POST',
178+
headers: { Authorization: 'Bearer valid-key' },
179+
body: JSON.stringify({
180+
toolName: 'COMPOSIO_REMOTE_WORKBENCH',
181+
input: {},
182+
}),
183+
})
184+
185+
const response = await postComposioExecute({
186+
req,
187+
getUserInfoFromApiKey,
188+
db: mockDb,
189+
logger,
190+
loggerWithContext,
191+
executeTool,
192+
checkRateLimit: mock(() => ({ limited: false as const })),
193+
isConfigured: () => true,
194+
})
195+
196+
expect(response.status).toBe(400)
197+
expect(executeTool).not.toHaveBeenCalled()
198+
})
199+
172200
test('rejects unauthenticated Composio requests', async () => {
173201
const req = new NextRequest('http://localhost/api/v1/composio/execute', {
174202
method: 'POST',

web/src/app/api/v1/composio/execute/_post.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getErrorObject } from '@codebuff/common/util/error'
2+
import { COMPOSIO_META_TOOL_NAMES } from '@codebuff/common/constants/composio'
23
import { NextResponse } from 'next/server'
34
import { z } from 'zod/v4'
45

@@ -20,7 +21,7 @@ type CheckComposioRateLimitFn = typeof checkComposioRateLimit
2021
type IsComposioConfiguredFn = typeof isComposioConfigured
2122

2223
const composioExecuteBodySchema = z.object({
23-
toolName: z.string().min(1),
24+
toolName: z.enum(COMPOSIO_META_TOOL_NAMES),
2425
input: z.record(z.string(), z.unknown()).default({}),
2526
})
2627

web/src/server/composio.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ import { existsSync, readFileSync } from 'fs'
44
import { homedir } from 'os'
55
import path from 'path'
66

7-
import {
8-
COMPOSIO_API_KEY_ENV_VAR,
9-
COMPOSIO_META_TOOL_NAMES,
10-
} from '@codebuff/common/constants/composio'
7+
import { COMPOSIO_API_KEY_ENV_VAR } from '@codebuff/common/constants/composio'
118
import { getErrorObject } from '@codebuff/common/util/error'
129
import { env } from '@codebuff/internal/env'
1310
import * as schema from '@codebuff/internal/db/schema'
@@ -18,9 +15,9 @@ import type { Logger } from '@codebuff/common/types/contracts/logger'
1815
import type { JSONValue } from '@codebuff/common/types/json'
1916
import type { ToolResultOutput } from '@codebuff/common/types/messages/content-part'
2017
import type { CodebuffPgDatabase } from '@codebuff/internal/db/types'
18+
import type { ComposioMetaToolName } from '@codebuff/common/constants/composio'
2119

2220
const COMPOSIO_HOME_ENV_PATH = path.join(homedir(), 'codebuff', '.env.local')
23-
const allowedToolNames = new Set<string>(COMPOSIO_META_TOOL_NAMES)
2421

2522
type ComposioSession = Awaited<ReturnType<Composio['create']>>
2623
type ComposioClient = Composio
@@ -297,22 +294,11 @@ async function getSessionForUser(params: {
297294
export async function executeComposioTool(params: {
298295
db: CodebuffPgDatabase
299296
userId: string
300-
toolName: string
297+
toolName: ComposioMetaToolName
301298
input: Record<string, unknown>
302299
logger: Logger
303300
apiKey?: string
304301
}): Promise<ToolResultOutput[] | null> {
305-
if (!allowedToolNames.has(params.toolName)) {
306-
return [
307-
{
308-
type: 'json',
309-
value: {
310-
errorMessage: `Unsupported Composio tool: ${params.toolName}`,
311-
},
312-
},
313-
]
314-
}
315-
316302
const apiKey = params.apiKey ?? getComposioApiKey()
317303
if (!apiKey) return null
318304

0 commit comments

Comments
 (0)