Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ if (!process.env.AGENTUITY_API_KEY && !process.env.AGENTUITY_SDK_KEY) {
process.exit(1);
}

if (!process.env.AGENTUITY_PROJECT_KEY) {
console.error(
"\x1b[31m[ERROR] AGENTUITY_PROJECT_KEY is not set. Please set this environment variable.\x1b[0m"
);
process.exit(1);
}

if (!process.env.AGENTUITY_TRANSPORT_URL) {
console.warn(
"\x1b[31m[WARN] You are running this agent outside of the Agentuity environment. Any automatic Agentuity features will be disabled.\x1b[0m"
Expand All @@ -40,6 +47,55 @@ if (!process.env.AGENTUITY_TRANSPORT_URL) {
}
}

if (!process.env.ATTIO_AUTH_TOKEN) {
console.error(
"\x1b[31m[ERROR] ATTIO_AUTH_TOKEN is not set. Please set this environment variable.\x1b[0m"
);
process.exit(1);
}

if (!process.env.SLACK_WEBHOOK) {
console.error(
"\x1b[31m[ERROR] SLACK_WEBHOOK is not set. Please set this environment variable.\x1b[0m"
);
process.exit(1);
}

if (!process.env.SMARTLEAD_API_KEY) {
console.error(
"\x1b[31m[ERROR] SMARTLEAD_API_KEY is not set. Please set this environment variable.\x1b[0m"
);
process.exit(1);
}

if (!process.env.STRIPE_SIGNING_SECRET) {
console.error(
"\x1b[31m[ERROR] STRIPE_SIGNING_SECRET is not set. Please set this environment variable.\x1b[0m"
);
process.exit(1);
}

if (!process.env.STRIPE_API_KEY) {
console.error(
"\x1b[31m[ERROR] STRIPE_API_KEY is not set. Please set this environment variable.\x1b[0m"
);
process.exit(1);
}

if (!process.env.COMPOSIO_API_KEY) {
console.error(
"\x1b[31m[ERROR] COMPOSIO_API_KEY is not set. Please set this environment variable.\x1b[0m"
);
process.exit(1);
}

if (!process.env.COMPOSIO_USER_ID) {
console.error(
"\x1b[31m[ERROR] COMPOSIO_USER_ID is not set. Please set this environment variable.\x1b[0m"
);
process.exit(1);
}

runner(true, import.meta.dirname).catch((err) => {
if (err instanceof Error) {
console.error(err.message);
Expand Down
93 changes: 45 additions & 48 deletions lib/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { Composio } from "@composio/core";
import { AnthropicProvider } from "@composio/anthropic";
import { Anthropic } from "@anthropic-ai/sdk";

const client = new Anthropic();

const composio = new Composio({
apiKey: process.env.COMPOSIO_API_KEY,
provider: new AnthropicProvider(),
});

export const createAgent = (
prompt: string,
extraTools: any[] = [],
Expand All @@ -20,46 +27,33 @@ export const createAgent = (
resp: AgentResponse,
ctx: AgentContext
) {
const rawBody = await req.data.text();
if (verifyWebhook && !verifyWebhook(rawBody, req, resp, ctx)) {
const payload = await req.data.json();
if (
verifyWebhook &&
!verifyWebhook(JSON.stringify(payload), req, resp, ctx)
) {
Comment on lines +30 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify webhook signature validation compatibility.

The change from raw body to parsed JSON might break webhook signature validation if the verifier expects the exact raw body bytes for HMAC verification. Many webhook providers (like Stripe) require the raw body for signature validation.


🏁 Script executed:

#!/bin/bash
# Check if any webhook verifiers in the codebase use signature validation
rg -A 10 "verifyWebhook|signature|hmac" --type ts

Length of output: 2057


Ensure webhook signature verifiers receive the unaltered raw body

The handler currently parses the JSON and then re-serializes it for verifyWebhook, which will break HMAC checks (e.g. Stripe’s constructEvent) that require the exact raw payload bytes. Update the code to read the raw text first, pass it directly into verifyWebhook, then parse it for further processing.

Example diff:

-    const payload = await req.data.json();
-    if (
-      verifyWebhook &&
-      !verifyWebhook(JSON.stringify(payload), req, resp, ctx)
-    ) {
+    const rawBody = await req.data.text();
+    const payload = JSON.parse(rawBody);
+    if (
+      verifyWebhook &&
+      !verifyWebhook(rawBody, req, resp, ctx)
+    ) {
       return resp.json({
         success: false,
         error: "Webhook verification failed.",
       });

Areas to address:

  • lib/agent.ts (lines 29–33): switch from json() / stringify to text() / JSON.parse
  • src/agents/stripe-agent/index.ts: ensure your verifyWebhook implementation (currently commented out) accepts and uses the raw body for HMAC verification
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const payload = await req.data.json();
if (
verifyWebhook &&
!verifyWebhook(JSON.stringify(payload), req, resp, ctx)
) {
// Read the raw body for signature verification
const rawBody = await req.data.text();
const payload = JSON.parse(rawBody);
if (
verifyWebhook &&
!verifyWebhook(rawBody, req, resp, ctx)
) {
return resp.json({
success: false,
error: "Webhook verification failed.",
});
}
🤖 Prompt for AI Agents
In lib/agent.ts around lines 29 to 33, the code currently parses the request
body as JSON and then re-serializes it for verifyWebhook, which breaks HMAC
verification. Change the code to first read the raw request body as text, pass
this raw text directly to verifyWebhook, and then parse the text with JSON.parse
for further processing. This ensures the webhook signature verifier receives the
exact unaltered payload it requires.

return resp.json({
success: false,
error: "Webhook verification failed.",
});
}

const client = new Anthropic();

const composio = new Composio({
apiKey: process.env.COMPOSIO_API_KEY,
provider: new AnthropicProvider(),
});
const userId = process.env.COMPOSIO_USER_ID;
if (!userId) {
throw new Error("COMPOSIO_USER_ID is not set");
}

const REQUIRED_TOOLS = [
"ATTIO_FIND_RECORD",
"ATTIO_UPDATE_RECORD",
"ATTIO_CREATE_RECORD",
"ATTIO_LIST_RECORDS",
"ATTIO_GET_OBJECT",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL",
"getOrgIdFromCustomer",
"latestAttioNumber",
];

const allTools = await composio.tools.get("joel", {
toolkits: ["ATTIO", "SLACK"],
const tools = await composio.tools.get(userId, {
tools: [
"ATTIO_FIND_RECORD",
"ATTIO_UPDATE_RECORD",
"ATTIO_CREATE_RECORD",
"ATTIO_LIST_RECORDS",
"ATTIO_GET_OBJECT",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL",
],
});

const tools = allTools.filter((t) => REQUIRED_TOOLS.includes(t.name));
//console.log("tools: ", tools);

const payload = JSON.parse(rawBody); // parse it here because we read it as text for verification

// Note: Need to specify attribute names for the tool call: the default for email is "email" but it should be "email_addresses"

// Create a set of custom tool names for quick lookup
const customToolNames = new Set(extraTools.map((tool) => tool.name));

const maxIterations = 10;
let iteration = 0;
let previousToolCallResults: any[] = [];
Expand Down Expand Up @@ -106,7 +100,7 @@ ${
toolCalls,
null,
2
)}`
)}\nReason: ${rejectReason}`
: ""
}

Expand All @@ -127,10 +121,9 @@ ${
// Claude returns a list of content blocks in `response.content`
toolCalls = response.content.filter((block) => block.type === "tool_use");

if (toolCalls.length) {
// console.log("Tool calls", toolCalls);
} else {
console.log("No tool calls, done.");
// If there are no tool calls, we're done.
if (!toolCalls.length) {
ctx.logger.info("No tool calls detected, finishing up.");
const textBlock = response.content.find(
(block) => block.type === "text"
);
Expand All @@ -140,7 +133,6 @@ ${
//JUDGE THE TOOL CALLS HERE
const judgeResponse = await client.messages.create({
model: "claude-3-5-haiku-20241022", // Using cheaper Haiku for judge too
// Removed tools - judge should only return text, not make tool calls
max_tokens: 1000,
stream: false,
messages: [
Expand Down Expand Up @@ -179,32 +171,38 @@ Respond ONLY with the JSON decision object, no other text:
try {
judgeDecision = JSON.parse(judgeBlock.text);
} catch (error) {
console.log("Judge response that failed to parse:", judgeBlock.text);
ctx.logger.error(
"Judge response that failed to parse:",
judgeBlock.text
);
return resp.text(
`Judge response could not be parsed. Raw response: ${judgeBlock.text}`
);
}
} else {
console.log(
ctx.logger.info(
"Full judge response:",
JSON.stringify(judgeResponse.content, null, 2)
);
return resp.text("No judge response found.");
}

if (!judgeDecision) return resp.text("No judge decision.");

if (judgeDecision.decision === "reject") {
justRejected = true;
console.log("Rejected tool calls:", toolCalls, judgeDecision);
ctx.logger.info("Rejected tool calls:", toolCalls, judgeDecision);
rejectReason = judgeDecision.reason;
iteration++;
continue;
}

// If we get here, the Judge approved the tool calls.
// Judge approved the tool calls
justRejected = false;
rejectReason = "";

// Separate custom tools from composio tools
const customToolNames = new Set(extraTools.map((tool) => tool.name));

const customToolCalls = toolCalls.filter((call: any) =>
customToolNames.has(call.name)
);
Expand All @@ -216,14 +214,14 @@ Respond ONLY with the JSON decision object, no other text:

// Execute custom tools
if (customToolCalls.length > 0) {
console.log("Executing custom tools:", customToolCalls);
ctx.logger.info("Executing custom tools:", customToolCalls);
const customResults: any[] = [];

for (const toolCall of customToolCalls) {
const executor = customToolExecutors[toolCall.name];
if (executor) {
try {
const result = await executor(toolCall.input);
const result = await executor(toolCall.input); // This is where the tool call is executed based on its executor.
customResults.push({
tool_call_id: toolCall.id,
type: "tool_result",
Expand All @@ -247,18 +245,17 @@ Respond ONLY with the JSON decision object, no other text:
}
}

// If there are no composio tools, return the custom results - otherwise, merge them later
if (composioToolCalls.length === 0) {
// Only custom tools, return custom results
toolCallResult = customResults;
} else {
// Both custom and composio tools, we'll merge results
toolCallResult.customResults = customResults;
}
}

// Execute composio tools if any
if (composioToolCalls.length > 0) {
console.log("Executing composio tools:", composioToolCalls);
ctx.logger.info("Executing composio tools:", composioToolCalls);

// Create a response-like object with only composio tool calls
const composioResponse = {
Expand All @@ -277,7 +274,7 @@ Respond ONLY with the JSON decision object, no other text:
};

const composioResult = await composio.provider.handleToolCalls(
"joel",
userId,
composioResponse
);

Expand Down
Loading