From 4c249c579fb7d0d36cd365f9c96ff19bd6eba654 Mon Sep 17 00:00:00 2001 From: Chummy Date: Thu, 19 Feb 2026 23:42:58 +0800 Subject: [PATCH] fix(composio): repair v3 execute path and enable alias --- docs/config-reference.md | 14 ++++++++++ src/config/schema.rs | 13 +++++++++- src/tools/composio.rs | 55 ++++++++++++++++++++++++++++++++-------- 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/docs/config-reference.md b/docs/config-reference.md index 3635878..6be4d6a 100644 --- a/docs/config-reference.md +++ b/docs/config-reference.md @@ -62,6 +62,20 @@ Notes: - `reasoning_enabled = true` explicitly requests reasoning for supported providers (`think: true` on `ollama`). - Unset keeps provider defaults. +## `[composio]` + +| Key | Default | Purpose | +|---|---|---| +| `enabled` | `false` | Enable Composio managed OAuth tools | +| `api_key` | unset | Composio API key used by the `composio` tool | +| `entity_id` | `default` | Default `user_id` sent on connect/execute calls | + +Notes: + +- Backward compatibility: legacy `enable = true` is accepted as an alias for `enabled = true`. +- If `enabled = false` or `api_key` is missing, the `composio` tool is not registered. +- Typical flow: call `connect`, complete browser OAuth, then run `execute` for the desired tool action. + ## `[multimodal]` | Key | Default | Purpose | diff --git a/src/config/schema.rs b/src/config/schema.rs index f1fbb4f..011c329 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -648,7 +648,7 @@ impl Default for GatewayConfig { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct ComposioConfig { /// Enable Composio integration for 1000+ OAuth tools - #[serde(default)] + #[serde(default, alias = "enable")] pub enabled: bool, /// Composio API key (stored encrypted when secrets.encrypt = true) #[serde(default)] @@ -4418,6 +4418,17 @@ enabled = true assert_eq!(parsed.entity_id, "default"); } + #[test] + async fn composio_config_enable_alias_supported() { + let toml_str = r" +enable = true +"; + let parsed: ComposioConfig = toml::from_str(toml_str).unwrap(); + assert!(parsed.enabled); + assert!(parsed.api_key.is_none()); + assert_eq!(parsed.entity_id, "default"); + } + // ══════════════════════════════════════════════════════════ // SECRETS CONFIG TESTS // ══════════════════════════════════════════════════════════ diff --git a/src/tools/composio.rs b/src/tools/composio.rs index ed846f7..bf26748 100644 --- a/src/tools/composio.rs +++ b/src/tools/composio.rs @@ -137,12 +137,29 @@ impl ComposioTool { .await { Ok(result) => Ok(result), - Err(v3_err) => match self.execute_action_v2(action_name, params, entity_id).await { - Ok(result) => Ok(result), - Err(v2_err) => anyhow::bail!( - "Composio execute failed on v3 ({v3_err}) and v2 fallback ({v2_err})" - ), - }, + Err(v3_err) => { + let mut v2_candidates = vec![action_name.trim().to_string()]; + let legacy_action_name = normalize_legacy_action_name(action_name); + if !legacy_action_name.is_empty() && !v2_candidates.contains(&legacy_action_name) { + v2_candidates.push(legacy_action_name); + } + + let mut v2_errors = Vec::new(); + for candidate in v2_candidates { + match self + .execute_action_v2(&candidate, params.clone(), entity_id) + .await + { + Ok(result) => return Ok(result), + Err(v2_err) => v2_errors.push(format!("{candidate}: {v2_err}")), + } + } + + anyhow::bail!( + "Composio execute failed on v3 ({v3_err}) and v2 fallback attempts ({})", + v2_errors.join(" | ") + ); + } } } @@ -152,7 +169,7 @@ impl ComposioTool { entity_id: Option<&str>, connected_account_ref: Option<&str>, ) -> (String, serde_json::Value) { - let url = format!("{COMPOSIO_API_BASE_V3}/tools/{tool_slug}/execute"); + let url = format!("{COMPOSIO_API_BASE_V3}/tools/execute/{tool_slug}"); let account_ref = connected_account_ref.and_then(|candidate| { let trimmed_candidate = candidate.trim(); (!trimmed_candidate.is_empty()).then_some(trimmed_candidate) @@ -606,6 +623,10 @@ fn normalize_tool_slug(action_name: &str) -> String { action_name.trim().replace('_', "-").to_ascii_lowercase() } +fn normalize_legacy_action_name(action_name: &str) -> String { + action_name.trim().replace('-', "_").to_ascii_uppercase() +} + fn map_v3_tools_to_actions(items: Vec) -> Vec { items .into_iter() @@ -966,6 +987,18 @@ mod tests { ); } + #[test] + fn normalize_legacy_action_name_supports_v3_slug_input() { + assert_eq!( + normalize_legacy_action_name("gmail-fetch-emails"), + "GMAIL_FETCH_EMAILS" + ); + assert_eq!( + normalize_legacy_action_name(" GITHUB_LIST_REPOS "), + "GITHUB_LIST_REPOS" + ); + } + #[test] fn extract_redirect_url_supports_v2_and_v3_shapes() { let v2 = json!({"redirectUrl": "https://app.composio.dev/connect-v2"}); @@ -1041,10 +1074,10 @@ mod tests { #[test] fn composio_action_with_unicode() { - let json_str = r#"{"name": "SLACK_SEND_MESSAGE", "appName": "slack", "description": "Send message with emoji 🎉 and unicode 中文", "enabled": true}"#; + let json_str = r#"{"name": "SLACK_SEND_MESSAGE", "appName": "slack", "description": "Send message with emoji 🎉 and unicode Ω", "enabled": true}"#; let action: ComposioAction = serde_json::from_str(json_str).unwrap(); assert!(action.description.as_ref().unwrap().contains("🎉")); - assert!(action.description.as_ref().unwrap().contains("中文")); + assert!(action.description.as_ref().unwrap().contains("Ω")); } #[test] @@ -1093,7 +1126,7 @@ mod tests { assert_eq!( url, - "https://backend.composio.dev/api/v3/tools/gmail-send-email/execute" + "https://backend.composio.dev/api/v3/tools/execute/gmail-send-email" ); assert_eq!(body["arguments"]["to"], json!("test@example.com")); assert_eq!(body["user_id"], json!("workspace-user")); @@ -1111,7 +1144,7 @@ mod tests { assert_eq!( url, - "https://backend.composio.dev/api/v3/tools/github-list-repos/execute" + "https://backend.composio.dev/api/v3/tools/execute/github-list-repos" ); assert_eq!(body["arguments"], json!({})); assert!(body.get("connected_account_id").is_none());