{"record":{"id":"5abf62f9601eb730","repo":"zeroclaw-labs/zeroclaw","slug":"oauth2-configured-for-but-no-auth-service-pro","errorCode":null,"errorMessage":"oauth2 configured for '{}' but no auth service provided","messagePattern":"oauth2 configured for '(.+?)' but no auth service provided","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/email_imap.rs","lineNumber":112,"sourceCode":"    let stream = TlsStreamTolerant(raw);\n\n    let mut client = async_imap::Client::new(stream);\n    client.read_response().await.context(\"no IMAP greeting\")?;\n\n    if let Some(oauth2_cfg) = &cfg.oauth2 {\n        let token = if let Some(svc) = auth_service {\n            let channel_key = format!(\"email.{}\", alias);\n            svc.get_valid_email_oauth2_token(\n                &channel_key,\n                None,\n                &oauth2_cfg.token_url,\n                &oauth2_cfg.client_id,\n                &oauth2_cfg.scopes,\n            )\n            .await?\n            .ok_or_else(|| anyhow::Error::msg(format!(\"no OAuth2 token available for {}\", alias)))?\n        } else {\n            anyhow::bail!(\n                \"oauth2 configured for '{}' but no auth service provided\",\n                alias\n            );\n        };\n\n        struct XOAuth2 {\n            user: String,\n            token: String,\n        }\n        impl async_imap::Authenticator for XOAuth2 {\n            type Response = String;\n            fn process(&mut self, _: &[u8]) -> String {\n                format!(\"user={}\\x01auth=Bearer {}\\x01\\x01\", self.user, self.token)\n            }\n        }\n        client\n            .authenticate(\n                \"XOAUTH2\",","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/email_imap.rs#L94-L130","documentation":"The IMAP connection was configured for OAuth2 (cfg.oauth2 is Some) but imap_connect was called with auth_service: None, so there is no way to mint an XOAUTH2 bearer token. The email tool's contract is: an oauth2 section in the EmailConfig requires an AuthService that can call get_valid_email_oauth2_token; when it is absent the connection is refused before any SASL authentication is attempted (email_imap.rs:99-116).","triggerScenarios":"Calling imap_connect(&cfg, None, alias) where cfg has an [email.<alias>.oauth2] block (token_url, client_id, scopes). Typically the runtime constructs the email tool without wiring the shared zeroclaw_providers auth::AuthService dependency, or a test/embedded deployment builds EmailConfig from config but skips auth service initialization.","commonSituations":"Adding an oauth2 section to email config without rebuilding the tool pipeline that provides the auth service; running a minimal test harness that instantiates the email tool standalone; partial refactors that pass Option::None for the auth service; Gmail/Office365 accounts where password login is disabled and XOAUTH2 is mandatory.","solutions":["Wire the AuthService into the component that calls imap_connect so auth_service is Some whenever oauth2 is configured.","If password authentication is intended, remove the oauth2 section from the email alias config so the client.login branch (email_imap.rs:139-143) is taken.","Audit the tool factory: ensure the email tool is constructed with the same Arc<AuthService> used by other channels.","For dev environments without OAuth2, point the alias at a password-based test account."],"exampleFix":"// before\nlet session = imap_connect(&cfg, None, \"gmail\").await?;\n// cfg.oauth2 is Some -> bail: oauth2 configured for 'gmail' but no auth service provided\n\n// after\nlet auth = Arc::new(zeroclaw_providers::auth::AuthService::new(/* provider registry */));\nlet session = imap_connect(&cfg, Some(&auth), \"gmail\").await?;","handlingStrategy":"validation","validationCode":"// Call before imap_connect: oauth2 config demands an auth service.\nfn imap_config_is_callable(cfg: &EmailConfig, auth: Option<&Arc<AuthService>>) -> Result<(), String> {\n    if cfg.oauth2.is_some() && auth.is_none() {\n        return Err(\"email alias has oauth2 configured but no AuthService was provided; wire the auth service or remove the oauth2 block\".into());\n    }\n    Ok(())\n}","typeGuard":"fn oauth2_has_auth_service(cfg: &EmailConfig, auth: Option<&Arc<AuthService>>) -> bool {\n    !(cfg.oauth2.is_some() && auth.is_none())\n}","tryCatchPattern":"match imap_connect(&cfg, auth, alias).await {\n    Err(e) if e.to_string().contains(\"no auth service provided\") => {\n        // configuration bug, not transient: fail fast with a config-fix hint;\n        // do not retry\n    }\n    r => r,\n}","preventionTips":["Treat oauth2 config sections and AuthService wiring as one unit in code review","Add a startup config check that fails boot when any email alias has oauth2 but no auth service","Cover this path with a unit test so refactors cannot silently pass None"],"tags":["email","imap","oauth2","auth","wiring","config"],"backgroundTag":"oauth2-misconfiguration","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}