Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/agent-workflows/cursor-cloud-agents.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Cursor Cloud Agents — parallel workflow

Operator playbook for running 15–20 [Cursor Cloud Agents](https://docs.cursor.com/agents/cloud) in parallel against OpenHuman. Companion to [`codex-pr-checklist.md`](codex-pr-checklist.md); the same merge gates apply.
Operator playbook for running 15–20 [Cursor Cloud Agents](https://cursor.com/docs/cloud-agent) in parallel against OpenHuman. Companion to [`codex-pr-checklist.md`](codex-pr-checklist.md); the same merge gates apply.

This doc closes [`tinyhumansai/openhuman#1480`](https://github.com/tinyhumansai/openhuman/issues/1480).

Expand Down
47 changes: 47 additions & 0 deletions src/core/observability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@ pub enum ExpectedErrorKind {
McpServerNeedsAuth,
}

/// Classifies error strings that are expected runtime/user-state conditions.
///
/// Returns `None` for messages that should remain reportable to Sentry.
pub fn expected_error_kind(message: &str) -> Option<ExpectedErrorKind> {
let lower = message.to_ascii_lowercase();
// F2/F4: a managed-backend `errorCode` (#870) means the backend owns this
Expand Down Expand Up @@ -481,6 +484,18 @@ pub fn expected_error_kind(message: &str) -> Option<ExpectedErrorKind> {
if lower.contains("binary not found") {
return Some(ExpectedErrorKind::LocalAiBinaryMissing);
}
// TAURI-RUST-CGP (#4174): remote MCP servers return HTTP 401 when the
// user must complete OAuth/sign-in. `mcp_registry::connections` already
// stores this as `needs_auth` for the UI; the RPC boundary can still
// re-raise the typed `McpUnauthorizedError` display string. Demote that
// canonical envelope so Sentry does not page on expected user-state.
if lower.contains("mcp unauthorized for") && lower.contains("http 401") {
log::debug!(
"[observability] expected_error_kind matched MCP unauthorized HTTP 401; \
classifying as ProviderUserState"
);
return Some(ExpectedErrorKind::ProviderUserState);
}
// Check `is_provider_user_state_message` BEFORE `is_backend_user_error_message`:
// composio's "Toolkit X is not enabled" lands as a 4xx that both would
// match, and the more specific `ProviderUserState` bucket is the right
Expand Down Expand Up @@ -5366,6 +5381,38 @@ mod tests {
);
}

// ── TAURI-RUST-CGP (#4174): remote MCP server needs OAuth sign-in ───

#[test]
fn classifies_mcp_unauthorized_401_as_provider_user_state() {
// Canonical `McpUnauthorizedError` Display shape from
// `mcp_client::client`: the remote MCP server returned 401 and the
// connect path already stores `needs_auth` for the UI. The RPC
// re-report must demote to expected user-state rather than page
// Sentry for a sign-in requirement.
for msg in [
"MCP unauthorized for `https://youtube.run.tools` (HTTP 401)",
"MCP unauthorized for `https://youtube.run.tools` (HTTP 401; \
resource metadata: https://youtube.run.tools/.well-known/oauth-protected-resource)",
] {
assert_eq!(
expected_error_kind(msg),
Some(ExpectedErrorKind::ProviderUserState),
"MCP 401 auth-required state must demote: {msg}"
);
}
}

#[test]
fn does_not_classify_unrelated_401_as_mcp_user_state() {
let unrelated = "GitHub API error: HTTP 401: Bad credentials";
assert_ne!(
expected_error_kind(unrelated),
Some(ExpectedErrorKind::ProviderUserState),
"generic 401s without the MCP unauthorized envelope must stay reportable"
);
}

// ── TAURI-RUST-34H: backend-wrapped Cloudflare anti-bot interstitial ─

#[test]
Expand Down
Loading