refactor(composio): extract derive_toolkit_slug helper (#3035)

This commit is contained in:
YOMXXX
2026-06-01 20:11:25 -07:00
committed by GitHub
parent 019038b688
commit 12fd0f7c63
8 changed files with 169 additions and 23 deletions
+15 -10
View File
@@ -121,12 +121,21 @@ fn prefix_class(class: ComposioErrorClass, body: &str) -> String {
format!("[composio:error:{}] {}", class.as_str(), body)
}
fn format_insufficient_scope_message(tool: &str, detail: &str) -> String {
let toolkit = tool
.split('_')
/// Derive the lowercase toolkit slug from a Composio tool/trigger identifier.
///
/// Identifiers are upper-snake-case (e.g. `GMAIL_NEW_GMAIL_MESSAGE`); the leading
/// segment names the toolkit, so this returns e.g. `gmail`. `str::split('_').next()`
/// always yields `Some(_)` for `&str` input (empty input yields `Some("")`), so
/// `unwrap_or_default()` is a safe, equivalent terminator.
fn derive_toolkit_slug(tool: &str) -> String {
tool.split('_')
.next()
.unwrap_or("integration")
.to_ascii_lowercase();
.unwrap_or_default()
.to_ascii_lowercase()
}
fn format_insufficient_scope_message(tool: &str, detail: &str) -> String {
let toolkit = derive_toolkit_slug(tool);
format!(
"`{tool}` was rejected because the connected {toolkit} account is missing required \
permissions ({detail}). Reconnect the integration in Settings → Connections → \
@@ -140,11 +149,7 @@ fn format_insufficient_scope_message(tool: &str, detail: &str) -> String {
/// [`format_insufficient_scope_message`] does (e.g. `GMAIL_NEW_GMAIL_MESSAGE`
/// → `gmail`), so the copy is branded and points the user at reconnecting.
fn format_trigger_permission_message(tool: &str) -> String {
let toolkit = tool
.split('_')
.next()
.unwrap_or("integration")
.to_ascii_lowercase();
let toolkit = derive_toolkit_slug(tool);
format!(
"Couldn't enable this trigger: the connected {toolkit} account doesn't have \
permission to manage triggers. Reconnect {toolkit} in Settings → Connections → \
+21 -1
View File
@@ -1,7 +1,27 @@
use super::{
classify_composio_error, format_provider_error, remap_transport_error, ComposioErrorClass,
classify_composio_error, derive_toolkit_slug, format_provider_error, remap_transport_error,
ComposioErrorClass,
};
// ── derive_toolkit_slug (issue #2913 nitpick — shared slug extraction) ──
#[test]
fn derive_toolkit_slug_extracts_leading_segment_lowercased() {
assert_eq!(derive_toolkit_slug("GMAIL_NEW_GMAIL_MESSAGE"), "gmail");
}
#[test]
fn derive_toolkit_slug_single_segment_is_lowercased() {
assert_eq!(derive_toolkit_slug("SLACK"), "slack");
}
#[test]
fn derive_toolkit_slug_empty_input_returns_empty_not_fallback() {
// Behavior-parity guard: `"".split('_').next()` yields `Some("")`, so the
// `unwrap_or("integration")` fallback does NOT apply — preserve that exactly.
assert_eq!(derive_toolkit_slug(""), "");
}
#[test]
fn classifies_gmail_insufficient_scope() {
let msg = "HTTP 403: Request had insufficient authentication scopes.";
@@ -155,8 +155,7 @@ pub fn capability_matrix() -> Vec<ComposioCapability> {
/// Lookup key is the lowercased prefix returned by
/// [`toolkit_from_slug`] applied to the action slug — e.g.
/// `GOOGLECALENDAR_CREATE_EVENT` → `"googlecalendar"`. Multi-segment
/// prefixes like `MICROSOFT_TEAMS_*` are matched via their first
/// segment with an extra arm.
/// prefixes like `MICROSOFT_TEAMS_*` return their known toolkit slug.
/// Synchronous visibility check for a Composio action slug given a
/// pre-loaded user scope preference.
///
@@ -200,7 +199,8 @@ pub fn catalog_for_toolkit(toolkit: &str) -> Option<&'static [CuratedTool]> {
"googledocs" | "google_docs" => Some(catalogs::GOOGLEDOCS_CURATED),
"googlesheets" | "google_sheets" => Some(catalogs::GOOGLESHEETS_CURATED),
"outlook" => Some(catalogs::OUTLOOK_CURATED),
// MICROSOFT_TEAMS_* slugs extract to "microsoft" via toolkit_from_slug.
// Keep the legacy "microsoft" alias while toolkit_from_slug now
// returns the precise "microsoft_teams" slug for Teams actions.
"microsoft" | "microsoft_teams" => Some(catalogs::MICROSOFT_TEAMS_CURATED),
"jira" => Some(catalogs::JIRA_CURATED),
"trello" => Some(catalogs::TRELLO_CURATED),
+23 -1
View File
@@ -125,7 +125,13 @@ impl ToolMemoryRule {
/// Generate a fresh, opaque rule id.
pub fn generate_id() -> String {
uuid::Uuid::new_v4().to_string()
let mut id = String::with_capacity(33);
id.push('r');
for byte in uuid::Uuid::new_v4().as_bytes() {
id.push((b'a' + (byte >> 4)) as char);
id.push((b'a' + (byte & 0x0f)) as char);
}
id
}
/// Storage key used inside the tool namespace.
@@ -207,6 +213,22 @@ mod tests {
assert_ne!(a, b);
}
#[test]
fn generated_rule_ids_are_safe_memory_document_keys() {
for _ in 0..128 {
let id = ToolMemoryRule::generate_id();
assert!(
id.chars().all(|ch| ch.is_ascii_lowercase()),
"generated id should avoid PII-shaped digits and separators: {id}"
);
let key = ToolMemoryRule::storage_key(&id);
assert!(
!crate::openhuman::memory_store::safety::pii::has_likely_pii(&key),
"generated storage key should not trip PII boundary: {key}"
);
}
}
#[test]
fn rule_storage_key_uses_rule_prefix() {
assert_eq!(ToolMemoryRule::storage_key("abc"), "rule/abc");