{"record":{"id":"060122b0a30c71e3","repo":"tinyhumansai/openhuman","slug":"composio-authorize-authorize-oauth-scopes-field-060122","errorCode":null,"errorMessage":"composio.authorize: {AUTHORIZE_OAUTH_SCOPES_FIELD} must be a string or array","messagePattern":"composio\\.authorize: (.+?) must be a string or array","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/openhuman/integrations/composio/client.rs","lineNumber":640,"sourceCode":"            .map(ToString::to_string)\n            .collect(),\n        Value::Array(items) => {\n            let mut out = Vec::with_capacity(items.len() + required.len());\n            for item in items {\n                let Some(scope) = item.as_str() else {\n                    anyhow::bail!(\n                        \"composio.authorize: {AUTHORIZE_OAUTH_SCOPES_FIELD} entries must be strings\"\n                    );\n                };\n                let scope = scope.trim();\n                if !scope.is_empty() {\n                    out.push(scope.to_string());\n                }\n            }\n            out\n        }\n        _ => {\n            anyhow::bail!(\n                \"composio.authorize: {AUTHORIZE_OAUTH_SCOPES_FIELD} must be a string or array\"\n            );\n        }\n    };\n\n    for scope in required {\n        if !scopes.iter().any(|existing| existing == scope) {\n            scopes.push((*scope).to_string());\n        }\n    }\n    *value = json!(scopes);\n    Ok(())\n}\n\n/// Backend-mode [`ComposioClient`] constructor. **Internal to the\n/// composio module** — external callers should use\n/// [`create_composio_client`] (factory) or\n/// [`crate::openhuman::agent::harness::subagent_runner::user_is_signed_in_to_composio`]","sourceCodeStart":622,"sourceCodeEnd":658,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/integrations/composio/client.rs#L622-L658","documentation":"The shape-level guard of merge_required_oauth_scopes: the oauth_scopes field must be a JSON string (comma/whitespace separated, which the client splits) or an array of strings. Any other JSON type — object, number, boolean — bails client-side before the authorize request is sent.","triggerScenarios":"Calling authorize with oauth_scopes set to a non-string, non-array value, e.g. {\"oauth_scopes\": 3}, {\"oauth_scopes\": true}, or {\"oauth_scopes\": {\"read\": true}}.","commonSituations":"Config schema loosened so any JSON is accepted for the field; a count or flag accidentally stored under the scopes key; object-style scope maps (\"read\": true) forwarded where a list is expected.","solutions":["Fix the producer to emit either a comma/space-separated string or an array of scope strings","If you have an object-style scope map, flatten it to an array of granted scope names before calling","Validate the oauth_scopes shape at config load time (null/string/array-of-strings only)"],"exampleFix":"// before\nlet extra = json!({ \"oauth_scopes\": {\"repo\": true} });\nclient.authorize(\"github\", Some(extra)).await?;\n\n// after — flatten a scope map into the accepted array form\nlet scopes: Vec<String> = scope_map\n    .iter()\n    .filter(|(_, v)| v.as_bool().unwrap_or(false))\n    .map(|(k, _)| k.clone())\n    .collect();\nlet extra = json!({ \"oauth_scopes\": scopes });\nclient.authorize(\"github\", Some(extra)).await?;","handlingStrategy":"type-guard","validationCode":"// Normalize any scope representation into the accepted string|array-of-strings shape\nfn normalize_oauth_scopes(v: &serde_json::Value) -> serde_json::Value {\n    match v {\n        serde_json::Value::Null => serde_json::Value::Array(vec![]),\n        serde_json::Value::String(_) => v.clone(),\n        serde_json::Value::Array(_) => v.clone(),\n        serde_json::Value::Object(map) => serde_json::Value::Array(\n            map.iter()\n                .filter(|(_, v)| v.as_bool().unwrap_or(false))\n                .map(|(k, _)| serde_json::json!(k))\n                .collect(),\n        ),\n        _ => serde_json::Value::Array(vec![]), // numbers/bools carry no scopes\n    }\n}","typeGuard":"fn oauth_scopes_shape_ok(v: &serde_json::Value) -> bool {\n    matches!(v, serde_json::Value::Null | serde_json::Value::String(_))\n        || v.as_array().is_some_and(|a| a.iter().all(|i| i.is_string()))\n}","tryCatchPattern":null,"preventionTips":["Constrain the oauth_scopes config field to string or array-of-strings at schema level","Flatten object-style scope maps ({\"read\": true}) into arrays of names before forwarding","Add a unit test pinning the accepted shapes so accidental schema loosening fails in CI"],"tags":["composio","authorize","oauth","scopes","type-validation"],"backgroundTag":"json-type-mismatch","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}