diff --git a/backend/packages/harness/deerflow/models/credential_loader.py b/backend/packages/harness/deerflow/models/credential_loader.py index 9e16c43c3..05f839ed1 100644 --- a/backend/packages/harness/deerflow/models/credential_loader.py +++ b/backend/packages/harness/deerflow/models/credential_loader.py @@ -160,10 +160,15 @@ def _extract_claude_code_credential(data: dict[str, Any], source: str) -> Claude logger.debug("Claude Code credentials container exists but no accessToken found") return None + expires_at = oauth.get("expiresAt", 0) + if not isinstance(expires_at, (int, float)): + logger.debug("Claude Code credentials source %s has a non-numeric expiresAt; skipping", source) + return None + cred = ClaudeCodeCredential( access_token=access_token, refresh_token=oauth.get("refreshToken", ""), - expires_at=oauth.get("expiresAt", 0), + expires_at=expires_at, source=source, ) diff --git a/backend/tests/test_credential_loader.py b/backend/tests/test_credential_loader.py index b9a1b4580..2df804f68 100644 --- a/backend/tests/test_credential_loader.py +++ b/backend/tests/test_credential_loader.py @@ -300,6 +300,62 @@ def test_load_claude_code_credential_falls_back_to_default_when_override_contain assert cred.source == "claude-cli-file" +@pytest.mark.parametrize( + "expires_at", + [ + "1773430695128", + None, + [], + {}, + ], +) +def test_load_claude_code_credential_ignores_non_numeric_expires_at(tmp_path, monkeypatch, expires_at): + _clear_claude_code_env(monkeypatch) + monkeypatch.setenv("HOME", str(tmp_path)) + cred_file = tmp_path / "credentials.json" + cred_file.write_text( + json.dumps({"claudeAiOauth": {"accessToken": "sk-ant-oat01-test", "expiresAt": expires_at}}), + 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_expires_at_is_non_numeric(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": {"accessToken": "sk-ant-oat01-override", "expiresAt": "1773430695128"}}), + 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(