Skip to main content

agentzero_tools/
composio.rs

1use 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/// Composio integration tool for external action execution.
16///
17/// Composio provides a unified API for executing actions across third-party
18/// services (GitHub, Slack, Jira, etc.). This tool sends action requests
19/// to the Composio API.
20///
21/// Requires `COMPOSIO_API_KEY` environment variable or `api_key` in input.
22#[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        // Ensure env var is not set for this test
121        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        // Restore env var if it was set
132        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}