{"record":{"id":"cef1ae14ff3cfd06","repo":"Hmbown/CodeWhale","slug":"external-credential-consent-source-mismatch-for","errorCode":null,"errorMessage":"external credential consent source mismatch for {} (expected {})","messagePattern":"external credential consent source mismatch for (.+?) \\(expected (.+?)\\)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/config/src/external_credentials.rs","lineNumber":428,"sourceCode":"                provider.as_str()\n            );\n        }\n        if self.consent_version != EXTERNAL_CREDENTIAL_CONSENT_VERSION {\n            bail!(\n                \"external credential consent for {} uses unsupported version {}; revoke and consent again\",\n                provider.as_str(),\n                self.consent_version\n            );\n        }\n        if self.provider != provider.as_str() {\n            bail!(\n                \"external credential consent is scoped to provider {:?}, not {}\",\n                self.provider,\n                provider.as_str()\n            );\n        }\n        if self.source != source {\n            bail!(\n                \"external credential consent source mismatch for {} (expected {})\",\n                provider.as_str(),\n                source.as_str()\n            );\n        }\n        if !self.path.is_absolute() {\n            bail!(\n                \"external credential consent path for {} must be absolute\",\n                provider.as_str()\n            );\n        }\n        let normalized = resolve_external_credential_path(&self.path)?;\n        if normalized != self.path {\n            bail!(\n                \"external credential consent path for {} must be lexically normalized: {}\",\n                provider.as_str(),\n                quote_os_path(&self.path)\n            );","sourceCodeStart":410,"sourceCodeEnd":446,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/config/src/external_credentials.rs#L410-L446","documentation":"Part of the exact-tuple validation in validate_read_scope(): the consent's stored ExternalCredentialSource must equal the source the caller is requesting (e.g. a path-based file source vs another backend). A mismatch means the grant was minted for a different acquisition mechanism and is refused before any capability is issued.","triggerScenarios":"A consent record created for source = \"path\" is validated with source = another ExternalCredentialSource variant (or vice versa) — commonly after config copying or after a code path switched how it resolves the same provider's credential.","commonSituations":"Reusing a consent block when switching a provider from a file-based credential to a keyring/other source (or the reverse); edits to the source field; version changes renaming source variants so deserialized values no longer match.","solutions":["Re-create the consent with the source you now actually use","Align the code path and the consent: validate with the same source value the consent was created under"],"exampleFix":"# before\n[[external_credentials]]\nprovider = \"acme\"\nsource = \"keyring\"   # but the caller requests the path source\n\n# after\n[[external_credentials]]\nprovider = \"acme\"\nsource = \"path\"\npath = \"/keys/acme.env\"","handlingStrategy":"validation","validationCode":"if consent.source != requested_source {\n    // grant minted for a different acquisition mechanism; re-consent\n    return Err(anyhow!(\"consent source mismatch: recreate for {requested_source:?}\"));\n}","typeGuard":"fn matches_source(consent: &ExternalCredentialConsent, s: ExternalCredentialSource) -> bool {\n    consent.source == s\n}","tryCatchPattern":"match consent.validate_read_scope(provider, source, &path) {\n    Ok(()) => read_external_credential(&path),\n    Err(e) if e.to_string().contains(\"source mismatch\") => {\n        reconsent_with_source(provider, source, &path).await\n    }\n    Err(e) => Err(e),\n}","preventionTips":["When switching a provider's credential mechanism, re-consent under the new source","Do not mix keyring and path consents for the same provider entry","Validate the full (provider, source, path) tuple in tooling before reads"],"tags":["security","consent","source-mismatch","credentials"],"backgroundTag":"authorization-scope-mismatch","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}