{"record":{"id":"7f896b7227c5bb1e","repo":"Hmbown/CodeWhale","slug":"agy-oauth-token-member-member-is-empty","errorCode":null,"errorMessage":"agy OAuth token member `{member}` is empty","messagePattern":"agy OAuth token member `(.+?)` is empty","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/agy_credentials.rs","lineNumber":219,"sourceCode":"\n/// The stored value is opaque to Codewhale. Accept only shapes observed in\n/// the official store — a bare token string or a JSON object with a token\n/// member — and never synthesize or trim secrets beyond whitespace.\npub(crate) fn parse_agy_oauth_token_value(value: Option<String>) -> Result<Option<String>> {\n    let Some(raw) = value else {\n        return Ok(None);\n    };\n    let trimmed = raw.trim();\n    if trimmed.is_empty() {\n        return Ok(None);\n    }\n    if trimmed.starts_with('{') {\n        let parsed: serde_json::Value = serde_json::from_str(trimmed)\n            .with_context(|| \"agy OAuth token value is malformed JSON\")?;\n        for member in [\"access_token\", \"accessToken\", \"token\"] {\n            if let Some(token) = parsed.get(member).and_then(|v| v.as_str()) {\n                if token.trim().is_empty() {\n                    bail!(\"agy OAuth token member `{member}` is empty\");\n                }\n                return Ok(Some(token.to_string()));\n            }\n        }\n        bail!(\"agy OAuth token JSON carries no access token member\");\n    }\n    Ok(Some(trimmed.to_string()))\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use codewhale_config::ExternalCredentialReadGrant;\n    use std::collections::HashMap;\n\n    fn grant_for(path: &Path) -> ExternalCredentialReadGrant {\n        codewhale_config::ExternalCredentialConsentToml::read_only(\n            codewhale_config::ProviderKind::Antigravity,","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/agy_credentials.rs#L201-L237","documentation":"After extracting the raw token value from the agy store, parse_agy_oauth_token_value finds it is JSON, locates one of the accepted members (`access_token`, `accessToken`, `token`), but that member is an empty or whitespace-only string. An empty token can never authenticate, so the parser bails instead of returning a value that would fail later with a confusing 401.","triggerScenarios":"The state.vscdb token row holds JSON like `{\"access_token\":\"\"}` or `{\"accessToken\":\"  \"}` — typically a revoked, cleared, or half-written session from the agy client.","commonSituations":"User signed out of Antigravity but the row remains with blanked fields; a token refresh wrote an empty value before dying; a partially-synced profile.","solutions":["Re-authenticate in the agy/Antigravity client so a non-empty token is written, then re-import.","Pre-check the token value before import: parse the JSON yourself and require a non-blank `access_token`/`accessToken`/`token`.","Treat as 'no credentials' in the UX: prompt for login rather than showing a parse error."],"exampleFix":"// before\nlet token = antigravity_oauth_token_from_grant(&grant)?; // bails: member is empty\n\n// after\nlet token = match antigravity_oauth_token_from_grant(&grant) {\n    Ok(t) => t,\n    Err(e) if e.to_string().contains(\"is empty\") => {\n        return Ok(None); // treat as signed-out; prompt for login\n    }\n    Err(e) => return Err(e),\n};","handlingStrategy":"fallback","validationCode":"let raw = peek_token_value(grant.path())?; // read-only helper you control\nif let Some(v) = raw.as_deref() {\n    if let Ok(json) = serde_json::from_str::<serde_json::Value>(v.trim()) {\n        for member in [\"access_token\", \"accessToken\", \"token\"] {\n            if let Some(t) = json.get(member).and_then(|x| x.as_str()) {\n                if t.trim().is_empty() { /* signed out; prompt login */ }\n            }\n        }\n    }\n}","typeGuard":"fn has_nonempty_token_member(v: &serde_json::Value) -> bool {\n    [\"access_token\", \"accessToken\", \"token\"]\n        .iter()\n        .any(|m| v.get(*m).and_then(|x| x.as_str()).is_some_and(|s| !s.trim().is_empty()))\n}","tryCatchPattern":"match antigravity_oauth_token_from_grant(&grant) {\n    Ok(t) => t,\n    Err(e) if e.to_string().contains(\"is empty\") => { /* treat as signed out; start login flow */ None }\n    Err(e) => return Err(e),\n}","preventionTips":["Verify the agy client shows a signed-in session before importing.","Map 'empty token member' errors to a sign-in prompt, not a generic parse failure.","Re-import after re-authenticating; do not cache blanked tokens."],"tags":["antigravity","oauth","token","credentials"],"backgroundTag":"empty-oauth-token","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}