Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/openhuman/agent/agents/integrations_agent/prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
10 changes: 10 additions & 0 deletions src/openhuman/agent/agents/integrations_agent/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/openhuman/agent/agents/orchestrator/prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions src/openhuman/agent/agents/orchestrator/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions src/openhuman/app_state/ops_tests.rs
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/openhuman/composio/error_mapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
}

Expand Down
19 changes: 18 additions & 1 deletion src/openhuman/composio/error_mapping_tests.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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";
Expand Down
Loading