diff --git a/backend/packages/harness/deerflow/models/credential_loader.py b/backend/packages/harness/deerflow/models/credential_loader.py index 1dc1a2621..dc94335aa 100644 --- a/backend/packages/harness/deerflow/models/credential_loader.py +++ b/backend/packages/harness/deerflow/models/credential_loader.py @@ -148,7 +148,13 @@ def _iter_claude_code_credential_paths() -> list[Path]: def _extract_claude_code_credential(data: dict[str, Any], source: str) -> ClaudeCodeCredential | None: - oauth = data.get("claudeAiOauth", {}) + if not isinstance(data, dict): + logger.debug("Claude Code credentials source %s is not a JSON object; skipping", source) + return None + oauth = data.get("claudeAiOauth") + if not isinstance(oauth, dict): + logger.debug("Claude Code credentials source %s has a non-object claudeAiOauth container; skipping", source) + return None access_token = oauth.get("accessToken", "") if not access_token: logger.debug("Claude Code credentials container exists but no accessToken found") diff --git a/backend/tests/test_credential_loader.py b/backend/tests/test_credential_loader.py index d568133a7..2fc283caa 100644 --- a/backend/tests/test_credential_loader.py +++ b/backend/tests/test_credential_loader.py @@ -248,6 +248,57 @@ def test_load_claude_code_credential_falls_back_to_default_file_when_override_is assert cred.source == "claude-cli-file" +@pytest.mark.parametrize( + "payload", + [ + {"claudeAiOauth": None}, + {"claudeAiOauth": "sk-ant-oat01-raw"}, + {"claudeAiOauth": []}, + {"claudeAiOauth": 5}, + [], + ], +) +def test_load_claude_code_credential_ignores_malformed_oauth_container(tmp_path, monkeypatch, payload): + _clear_claude_code_env(monkeypatch) + monkeypatch.setenv("HOME", str(tmp_path)) + cred_file = tmp_path / "credentials.json" + cred_file.write_text(json.dumps(payload), encoding="utf-8") + monkeypatch.setenv("CLAUDE_CODE_CREDENTIALS_PATH", str(cred_file)) + + assert load_claude_code_credential() is None + + +def test_load_claude_code_credential_falls_back_to_default_when_override_container_is_malformed(tmp_path, monkeypatch): + _clear_claude_code_env(monkeypatch) + monkeypatch.setenv("HOME", str(tmp_path)) + + override_path = tmp_path / "credentials.json" + override_path.write_text(json.dumps({"claudeAiOauth": None}), encoding="utf-8") + monkeypatch.setenv("CLAUDE_CODE_CREDENTIALS_PATH", str(override_path)) + + default_path = tmp_path / ".claude" / ".credentials.json" + default_path.parent.mkdir() + default_path.write_text( + json.dumps( + { + "claudeAiOauth": { + "accessToken": "sk-ant-oat01-default", + "refreshToken": "sk-ant-ort01-default", + "expiresAt": 4_102_444_800_000, + } + } + ), + encoding="utf-8", + ) + + cred = load_claude_code_credential() + + assert cred is not None + assert cred.access_token == "sk-ant-oat01-default" + assert cred.refresh_token == "sk-ant-ort01-default" + assert cred.source == "claude-cli-file" + + def test_load_codex_cli_credential_supports_nested_tokens_shape(tmp_path, monkeypatch): auth_path = tmp_path / "auth.json" auth_path.write_text(