fix(composio): surface Gmail scope errors as permissions (#2414)

This commit is contained in:
Aqil Aziz
2026-05-21 17:23:44 +05:30
committed by GitHub
parent 6137b67811
commit ec9708ac6f
7 changed files with 53 additions and 5 deletions
@@ -15,13 +15,15 @@ You do **not** have shell, file I/O, or any other capability beyond these permit
1. You already have the toolkit's action tools in your tool list — start there. If you need a schema reminder or a slug you don't see, call `composio_list_tools`.
2. Call the per-action tool (or `composio_execute` with the slug) using the caller's task as your guide.
3. If the call fails with an authentication / authorization / connection error, stop and return: **"Connection error, try to authenticate"** — the orchestrator will take over and route the user to settings.
3. If the call fails with `[composio:error:insufficient_scope]`, `insufficient authentication scopes`, or `missing required permissions`, do **not** call the service disconnected. Say the connected account is missing the permissions needed for the requested action and point the user to Settings → Connections → the toolkit to reconnect or enable the required scope.
4. If the call fails with a true authentication / authorization / connection error that is **not** a scope or permission error, stop and return: **"Connection error, try to authenticate"** — the orchestrator will take over and route the user to settings.
## Rules
- **Never fabricate action slugs.** Pull them from `composio_list_tools` or use the per-action tools already in your list.
- **Respect rate limits** — Composio and upstream providers both throttle. Back off on errors rather than retrying tightly.
- **Auth errors bubble up.** On any auth / connection failure reply exactly: `Connection error, try to authenticate`. Do not retry, do not attempt to re-authorise yourself — you have no tools for that.
- **Scope errors are not disconnections.** If Gmail or another connected toolkit returns insufficient scope / missing permissions, report the missing permission plainly and direct the user to Settings → Connections → that toolkit. Never say the toolkit is disconnected for this case.
- **Auth errors bubble up.** On true auth / connection failures only, reply exactly: `Connection error, try to authenticate`. Do not retry, do not attempt to re-authorise yourself — you have no tools for that.
- **Be precise** — every action expects a specific argument shape. Validate against the schema before calling.
- **Report results** — state what action was taken and the outcome, including any cost reported by Composio.
@@ -257,6 +257,16 @@ mod tests {
assert!(!body.contains("spawn_subagent"));
}
#[test]
fn build_distinguishes_scope_errors_from_disconnected_auth() {
let body = build(&ctx_with(&[], &[])).unwrap();
assert!(body.contains("[composio:error:insufficient_scope]"));
assert!(body.contains("Scope errors are not disconnections"));
assert!(body.contains("Never say the toolkit is disconnected"));
assert!(body.contains("Settings"));
assert!(body.contains("Connections"));
}
#[test]
fn build_skips_unconnected_integrations() {
let integrations = vec![ConnectedIntegration {
@@ -78,6 +78,7 @@ When the user asks to connect a service (Gmail, Notion, WhatsApp, Calendar, Driv
- **Never** explain OAuth, Composio, or any backend mechanic by name.
- Reply with one short bubble pointing to the in-app path: **Settings → Connections → [Service]**. Example: `head to Settings → Connections → Gmail to hook it up, ping me when it's connected`.
- If the user already said they connected it, call `composio_list_connections` to verify before continuing.
- Do **not** apply this rule to scope / permission failures such as `[composio:error:insufficient_scope]` or "missing required permissions". For those, say the connection exists but needs additional permissions in **Settings → Connections → [Service]**.
## Response Style
@@ -250,6 +250,16 @@ mod tests {
assert!(!body.contains("You have direct access"));
}
#[test]
fn build_does_not_route_scope_errors_as_disconnected() {
let body = build(&ctx_with(&[])).unwrap();
assert!(body.contains("[composio:error:insufficient_scope]"));
assert!(body.contains("missing required permissions"));
assert!(body.contains("connection exists but needs additional permissions"));
assert!(body.contains("Settings"));
assert!(body.contains("Connections"));
}
#[test]
fn delegation_guide_uses_compact_collapsed_format() {
let integrations = vec![ConnectedIntegration {
+8
View File
@@ -1,7 +1,11 @@
use super::*;
use once_cell::sync::Lazy as TestLazy;
use parking_lot::Mutex as TestMutex;
use serde_json::json;
use tempfile::tempdir;
static APP_STATE_CACHE_TEST_LOCK: TestLazy<TestMutex<()>> = TestLazy::new(|| TestMutex::new(()));
#[test]
fn sanitize_snapshot_user_drops_empty_payloads() {
assert_eq!(sanitize_snapshot_user(Some(json!({}))), None);
@@ -137,6 +141,7 @@ fn save_and_reload_stored_app_state_round_trips() {
#[test]
fn peek_cached_current_user_identity_plucks_known_fields() {
let _cache_lock = APP_STATE_CACHE_TEST_LOCK.lock();
struct CacheResetGuard;
impl Drop for CacheResetGuard {
fn drop(&mut self) {
@@ -164,6 +169,7 @@ fn peek_cached_current_user_identity_plucks_known_fields() {
#[test]
fn peek_cached_current_user_identity_returns_none_when_only_empty_fields_exist() {
let _cache_lock = APP_STATE_CACHE_TEST_LOCK.lock();
struct CacheResetGuard;
impl Drop for CacheResetGuard {
fn drop(&mut self) {
@@ -196,6 +202,7 @@ impl Drop for SnapshotCacheResetGuard {
#[test]
fn runtime_snapshot_cache_hit_within_ttl() {
let _cache_lock = APP_STATE_CACHE_TEST_LOCK.lock();
let _reset = SnapshotCacheResetGuard;
let dummy = build_dummy_runtime_snapshot();
@@ -215,6 +222,7 @@ fn runtime_snapshot_cache_hit_within_ttl() {
#[test]
fn runtime_snapshot_cache_miss_after_ttl() {
let _cache_lock = APP_STATE_CACHE_TEST_LOCK.lock();
let _reset = SnapshotCacheResetGuard;
*RUNTIME_SNAPSHOT_CACHE.lock() = Some(CachedRuntimeSnapshot {
+2 -2
View File
@@ -120,8 +120,8 @@ fn format_insufficient_scope_message(tool: &str, detail: &str) -> String {
.to_ascii_lowercase();
format!(
"`{tool}` was rejected because the connected {toolkit} account is missing required \
permissions ({detail}). Reconnect the integration in Settings → Skills and grant the \
scopes requested during OAuth."
permissions ({detail}). Reconnect the integration in Settings → Connections → \
{toolkit} and grant the scopes requested during OAuth."
)
}
+18 -1
View File
@@ -1,4 +1,6 @@
use super::{classify_composio_error, remap_transport_error, ComposioErrorClass};
use super::{
classify_composio_error, format_provider_error, remap_transport_error, ComposioErrorClass,
};
#[test]
fn classifies_gmail_insufficient_scope() {
@@ -9,6 +11,21 @@ fn classifies_gmail_insufficient_scope() {
);
}
#[test]
fn formats_gmail_insufficient_scope_as_missing_permissions_not_disconnected() {
let mapped = format_provider_error(
"GMAIL_SEND_EMAIL",
"HTTP 403: Request had insufficient authentication scopes.",
);
assert!(mapped.contains("[composio:error:insufficient_scope]"));
assert!(mapped.contains("connected gmail account is missing required permissions"));
assert!(mapped.contains("Settings"));
assert!(mapped.contains("Connections"));
assert!(mapped.contains("gmail"));
assert!(!mapped.contains("not connected"));
assert!(!mapped.contains("Settings → Skills"));
}
#[test]
fn classifies_slack_rate_limit() {
let msg = "Slack API error: ratelimited";