Skip to content

Commit 0118ea7

Browse files
aqilazizCodeGhost21
authored andcommitted
fix(composio): surface Gmail scope errors as permissions (tinyhumansai#2414)
1 parent 00cb90f commit 0118ea7

7 files changed

Lines changed: 53 additions & 5 deletions

File tree

src/openhuman/agent/agents/integrations_agent/prompt.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ You do **not** have shell, file I/O, or any other capability beyond these permit
1515

1616
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`.
1717
2. Call the per-action tool (or `composio_execute` with the slug) using the caller's task as your guide.
18-
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.
18+
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.
19+
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.
1920

2021
## Rules
2122

2223
- **Never fabricate action slugs.** Pull them from `composio_list_tools` or use the per-action tools already in your list.
2324
- **Respect rate limits** — Composio and upstream providers both throttle. Back off on errors rather than retrying tightly.
24-
- **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.
25+
- **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.
26+
- **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.
2527
- **Be precise** — every action expects a specific argument shape. Validate against the schema before calling.
2628
- **Report results** — state what action was taken and the outcome, including any cost reported by Composio.
2729

src/openhuman/agent/agents/integrations_agent/prompt.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,16 @@ mod tests {
257257
assert!(!body.contains("spawn_subagent"));
258258
}
259259

260+
#[test]
261+
fn build_distinguishes_scope_errors_from_disconnected_auth() {
262+
let body = build(&ctx_with(&[], &[])).unwrap();
263+
assert!(body.contains("[composio:error:insufficient_scope]"));
264+
assert!(body.contains("Scope errors are not disconnections"));
265+
assert!(body.contains("Never say the toolkit is disconnected"));
266+
assert!(body.contains("Settings"));
267+
assert!(body.contains("Connections"));
268+
}
269+
260270
#[test]
261271
fn build_skips_unconnected_integrations() {
262272
let integrations = vec![ConnectedIntegration {

src/openhuman/agent/agents/orchestrator/prompt.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ When the user asks to connect a service (Gmail, Notion, WhatsApp, Calendar, Driv
7878
- **Never** explain OAuth, Composio, or any backend mechanic by name.
7979
- 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`.
8080
- If the user already said they connected it, call `composio_list_connections` to verify before continuing.
81+
- 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]**.
8182

8283
## Response Style
8384

src/openhuman/agent/agents/orchestrator/prompt.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,16 @@ mod tests {
250250
assert!(!body.contains("You have direct access"));
251251
}
252252

253+
#[test]
254+
fn build_does_not_route_scope_errors_as_disconnected() {
255+
let body = build(&ctx_with(&[])).unwrap();
256+
assert!(body.contains("[composio:error:insufficient_scope]"));
257+
assert!(body.contains("missing required permissions"));
258+
assert!(body.contains("connection exists but needs additional permissions"));
259+
assert!(body.contains("Settings"));
260+
assert!(body.contains("Connections"));
261+
}
262+
253263
#[test]
254264
fn delegation_guide_uses_compact_collapsed_format() {
255265
let integrations = vec![ConnectedIntegration {

src/openhuman/app_state/ops_tests.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
use super::*;
2+
use once_cell::sync::Lazy as TestLazy;
3+
use parking_lot::Mutex as TestMutex;
24
use serde_json::json;
35
use tempfile::tempdir;
46

7+
static APP_STATE_CACHE_TEST_LOCK: TestLazy<TestMutex<()>> = TestLazy::new(|| TestMutex::new(()));
8+
59
#[test]
610
fn sanitize_snapshot_user_drops_empty_payloads() {
711
assert_eq!(sanitize_snapshot_user(Some(json!({}))), None);
@@ -137,6 +141,7 @@ fn save_and_reload_stored_app_state_round_trips() {
137141

138142
#[test]
139143
fn peek_cached_current_user_identity_plucks_known_fields() {
144+
let _cache_lock = APP_STATE_CACHE_TEST_LOCK.lock();
140145
struct CacheResetGuard;
141146
impl Drop for CacheResetGuard {
142147
fn drop(&mut self) {
@@ -164,6 +169,7 @@ fn peek_cached_current_user_identity_plucks_known_fields() {
164169

165170
#[test]
166171
fn peek_cached_current_user_identity_returns_none_when_only_empty_fields_exist() {
172+
let _cache_lock = APP_STATE_CACHE_TEST_LOCK.lock();
167173
struct CacheResetGuard;
168174
impl Drop for CacheResetGuard {
169175
fn drop(&mut self) {
@@ -196,6 +202,7 @@ impl Drop for SnapshotCacheResetGuard {
196202

197203
#[test]
198204
fn runtime_snapshot_cache_hit_within_ttl() {
205+
let _cache_lock = APP_STATE_CACHE_TEST_LOCK.lock();
199206
let _reset = SnapshotCacheResetGuard;
200207

201208
let dummy = build_dummy_runtime_snapshot();
@@ -215,6 +222,7 @@ fn runtime_snapshot_cache_hit_within_ttl() {
215222

216223
#[test]
217224
fn runtime_snapshot_cache_miss_after_ttl() {
225+
let _cache_lock = APP_STATE_CACHE_TEST_LOCK.lock();
218226
let _reset = SnapshotCacheResetGuard;
219227

220228
*RUNTIME_SNAPSHOT_CACHE.lock() = Some(CachedRuntimeSnapshot {

src/openhuman/composio/error_mapping.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ fn format_insufficient_scope_message(tool: &str, detail: &str) -> String {
120120
.to_ascii_lowercase();
121121
format!(
122122
"`{tool}` was rejected because the connected {toolkit} account is missing required \
123-
permissions ({detail}). Reconnect the integration in Settings → Skills and grant the \
124-
scopes requested during OAuth."
123+
permissions ({detail}). Reconnect the integration in Settings → Connections → \
124+
{toolkit} and grant the scopes requested during OAuth."
125125
)
126126
}
127127

src/openhuman/composio/error_mapping_tests.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use super::{classify_composio_error, remap_transport_error, ComposioErrorClass};
1+
use super::{
2+
classify_composio_error, format_provider_error, remap_transport_error, ComposioErrorClass,
3+
};
24

35
#[test]
46
fn classifies_gmail_insufficient_scope() {
@@ -9,6 +11,21 @@ fn classifies_gmail_insufficient_scope() {
911
);
1012
}
1113

14+
#[test]
15+
fn formats_gmail_insufficient_scope_as_missing_permissions_not_disconnected() {
16+
let mapped = format_provider_error(
17+
"GMAIL_SEND_EMAIL",
18+
"HTTP 403: Request had insufficient authentication scopes.",
19+
);
20+
assert!(mapped.contains("[composio:error:insufficient_scope]"));
21+
assert!(mapped.contains("connected gmail account is missing required permissions"));
22+
assert!(mapped.contains("Settings"));
23+
assert!(mapped.contains("Connections"));
24+
assert!(mapped.contains("gmail"));
25+
assert!(!mapped.contains("not connected"));
26+
assert!(!mapped.contains("Settings → Skills"));
27+
}
28+
1229
#[test]
1330
fn classifies_slack_rate_limit() {
1431
let msg = "Slack API error: ratelimited";

0 commit comments

Comments
 (0)