mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(composio): surface Gmail scope errors as permissions (#2414)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user