agentzero_tools/
composio.rs1use agentzero_core::{Tool, ToolContext, ToolResult};
2use anyhow::{anyhow, Context};
3use async_trait::async_trait;
4use serde::Deserialize;
5
6#[derive(Debug, Deserialize)]
7struct ComposioInput {
8 action: String,
9 #[serde(default)]
10 params: Option<serde_json::Value>,
11 #[serde(default)]
12 api_key: Option<String>,
13}
14
15#[derive(Debug, Default, Clone, Copy)]
23pub struct ComposioTool;
24
25#[async_trait]
26impl Tool for ComposioTool {
27 fn name(&self) -> &'static str {
28 "composio"
29 }
30
31 fn description(&self) -> &'static str {
32 "Execute actions via the Composio third-party integration API."
33 }
34
35 fn input_schema(&self) -> Option<serde_json::Value> {
36 Some(serde_json::json!({
37 "type": "object",
38 "properties": {
39 "action": { "type": "string", "description": "The Composio action to execute (e.g. github.star_repo)" },
40 "params": { "type": "object", "description": "Optional parameters for the action" },
41 "api_key": { "type": "string", "description": "Optional Composio API key (falls back to COMPOSIO_API_KEY env var)" }
42 },
43 "required": ["action"],
44 "additionalProperties": false
45 }))
46 }
47
48 async fn execute(&self, input: &str, _ctx: &ToolContext) -> anyhow::Result<ToolResult> {
49 let req: ComposioInput =
50 serde_json::from_str(input).context("composio expects JSON: {\"action\", ...}")?;
51
52 if req.action.trim().is_empty() {
53 return Err(anyhow!("action must not be empty"));
54 }
55
56 let api_key = req
57 .api_key
58 .or_else(|| std::env::var("COMPOSIO_API_KEY").ok())
59 .ok_or_else(|| anyhow!("COMPOSIO_API_KEY not set and no api_key provided in input"))?;
60
61 if api_key.trim().is_empty() {
62 return Err(anyhow!("api_key must not be empty"));
63 }
64
65 let params = req
66 .params
67 .unwrap_or(serde_json::Value::Object(Default::default()));
68
69 let client = reqwest::Client::new();
70 let response = client
71 .post("https://backend.composio.dev/api/v1/actions/execute")
72 .header("x-api-key", &api_key)
73 .json(&serde_json::json!({
74 "action": req.action,
75 "params": params,
76 }))
77 .send()
78 .await
79 .context("failed to reach Composio API")?;
80
81 let status = response.status();
82 let body = response
83 .text()
84 .await
85 .unwrap_or_else(|_| "(no body)".to_string());
86
87 if status.is_success() {
88 Ok(ToolResult { output: body })
89 } else {
90 Err(anyhow!(
91 "Composio API returned {}: {}",
92 status.as_u16(),
93 body
94 ))
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use agentzero_core::ToolContext;
103
104 fn test_ctx() -> ToolContext {
105 ToolContext::new("/tmp".to_string())
106 }
107
108 #[tokio::test]
109 async fn composio_rejects_empty_action() {
110 let tool = ComposioTool;
111 let err = tool
112 .execute(r#"{"action": ""}"#, &test_ctx())
113 .await
114 .expect_err("empty action should fail");
115 assert!(err.to_string().contains("action must not be empty"));
116 }
117
118 #[tokio::test]
119 async fn composio_rejects_missing_api_key() {
120 let had_key = std::env::var("COMPOSIO_API_KEY").ok();
122 std::env::remove_var("COMPOSIO_API_KEY");
123
124 let tool = ComposioTool;
125 let err = tool
126 .execute(r#"{"action": "github.star_repo"}"#, &test_ctx())
127 .await
128 .expect_err("missing api key should fail");
129 assert!(err.to_string().contains("COMPOSIO_API_KEY"));
130
131 if let Some(key) = had_key {
133 std::env::set_var("COMPOSIO_API_KEY", key);
134 }
135 }
136
137 #[tokio::test]
138 async fn composio_rejects_empty_api_key() {
139 let tool = ComposioTool;
140 let err = tool
141 .execute(
142 r#"{"action": "github.star_repo", "api_key": ""}"#,
143 &test_ctx(),
144 )
145 .await
146 .expect_err("empty api key should fail");
147 assert!(err.to_string().contains("api_key must not be empty"));
148 }
149}