diff --git a/apps/rowboat/app/lib/agents.ts b/apps/rowboat/app/lib/agents.ts index 6c6288dc..54db8734 100644 --- a/apps/rowboat/app/lib/agents.ts +++ b/apps/rowboat/app/lib/agents.ts @@ -7,6 +7,8 @@ import { CoreMessage, embed, generateText } from "ai"; import { ObjectId } from "mongodb"; import { z } from "zod"; import { Composio } from '@composio/core'; +import { SignJWT } from "jose"; +import crypto from "crypto"; // Internal dependencies import { embeddingModel } from '../lib/embedding'; @@ -167,6 +169,84 @@ async function invokeRagTool( return results; } +export async function invokeWebhookTool( + logger: PrefixLogger, + projectId: string, + name: string, + input: any, +): Promise { + logger = logger.child(`invokeWebhookTool`); + logger.log(`projectId: ${projectId}`); + logger.log(`name: ${name}`); + logger.log(`input: ${JSON.stringify(input)}`); + + const project = await projectsCollection.findOne({ + "_id": projectId, + }); + if (!project) { + throw new Error('Project not found'); + } + + if (!project.webhookUrl) { + throw new Error('Webhook URL not found'); + } + + // prepare request body + const toolCall: z.infer[number] = { + id: crypto.randomUUID(), + type: "function", + function: { + name, + arguments: JSON.stringify(input), + }, + } + const content = JSON.stringify({ + toolCall, + }); + const requestId = crypto.randomUUID(); + const bodyHash = crypto + .createHash('sha256') + .update(content, 'utf8') + .digest('hex'); + + // sign request + const jwt = await new SignJWT({ + requestId, + projectId, + bodyHash, + }) + .setProtectedHeader({ + alg: 'HS256', + typ: 'JWT', + }) + .setIssuer('rowboat') + .setAudience(project.webhookUrl) + .setSubject(`tool-call-${toolCall.id}`) + .setJti(requestId) + .setIssuedAt() + .setExpirationTime("5 minutes") + .sign(new TextEncoder().encode(project.secret)); + + // make request + const request = { + requestId, + content, + }; + const response = await fetch(project.webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-signature-jwt': jwt, + }, + body: JSON.stringify(request), + }); + if (!response.ok) { + throw new Error(`Failed to call webhook: ${response.status}: ${response.statusText}`); + } + const responseBody = await response.json(); + return responseBody; +} + // Helper to handle MCP tool calls async function invokeMcpTool( logger: PrefixLogger, @@ -296,6 +376,40 @@ function createMockTool( }); } +// Helper to create a webhook tool +function createWebhookTool( + logger: PrefixLogger, + config: z.infer, + projectId: string, +): Tool { + const { name, description, parameters } = config; + + return tool({ + name, + description, + strict: false, + parameters: { + type: 'object', + properties: parameters.properties, + required: parameters.required || [], + additionalProperties: true, + }, + async execute(input: any) { + try { + const result = await invokeWebhookTool(logger, projectId, name, input); + return JSON.stringify({ + result, + }); + } catch (error) { + logger.log(`Error executing webhook tool ${config.name}:`, error); + return JSON.stringify({ + error: `Tool execution failed: ${error}`, + }); + } + } + }); +} + // Helper to create an mcp tool function createMcpTool( logger: PrefixLogger, @@ -678,7 +792,8 @@ function createTools(logger: PrefixLogger, workflow: z.infer, t tools[toolName] = createMockTool(logger, config); logger.log(`created mock tool: ${toolName}`); } else { - logger.log(`unsupported tool type: ${toolName}`); + tools[toolName] = createWebhookTool(logger, config, workflow.projectId); + logger.log(`created webhook tool: ${toolName}`); } } return tools; diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/ToolsConfig.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/ToolsConfig.tsx index 35fd33e0..aa4f1128 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/ToolsConfig.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/ToolsConfig.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { Tabs, Tab } from '@/components/ui/tabs'; import { HostedServers } from './HostedServers'; import { CustomServers } from './CustomServers'; +import { WebhookConfig } from './WebhookConfig'; import { Composio } from './Composio'; import type { Key } from 'react'; @@ -62,6 +63,11 @@ export function ToolsConfig({ + +
+ +
+
);