From e2feca1e567d9bafce657de8ad8bc668bd7ad513 Mon Sep 17 00:00:00 2001 From: "Grapette.L" <90967079+Lesereingrape@users.noreply.github.com> Date: Sun, 27 Sep 2026 15:18:55 +0800 Subject: [PATCH] fix(models): skip credential files whose access token is not a string (#5864) * fix(models): skip credential files whose access token is not a string * fix(models): normalize the file-route access token and guard the refresh token --- .../deerflow/models/credential_loader.py | 19 ++- backend/tests/test_credential_loader.py | 137 ++++++++++++++++++ 2 files changed, 152 insertions(+), 4 deletions(-) diff --git a/backend/packages/harness/deerflow/models/credential_loader.py b/backend/packages/harness/deerflow/models/credential_loader.py index 5a623222d1..1615e8da75 100644 --- a/backend/packages/harness/deerflow/models/credential_loader.py +++ b/backend/packages/harness/deerflow/models/credential_loader.py @@ -156,8 +156,14 @@ def _extract_claude_code_credential(data: dict[str, Any], source: str) -> Claude logger.debug("Claude Code credentials source %s has a non-object claudeAiOauth container; skipping", source) return None access_token = oauth.get("accessToken", "") + if isinstance(access_token, str): + # The env and descriptor handoffs strip before they test the token, so a blank or + # padded value must not become a credential here either. + access_token = access_token.strip() + else: + access_token = "" if not access_token: - logger.debug("Claude Code credentials container exists but no accessToken found") + logger.debug("Claude Code credentials container exists but no string accessToken found") return None expires_at = oauth.get("expiresAt", 0) @@ -165,9 +171,14 @@ def _extract_claude_code_credential(data: dict[str, Any], source: str) -> Claude logger.debug("Claude Code credentials source %s has a non-numeric expiresAt; skipping", source) return None + refresh_token = oauth.get("refreshToken", "") + if not isinstance(refresh_token, str): + logger.debug("Claude Code credentials source %s has a non-string refreshToken; using no refresh token", source) + refresh_token = "" + cred = ClaudeCodeCredential( access_token=access_token, - refresh_token=oauth.get("refreshToken", ""), + refresh_token=refresh_token, expires_at=expires_at, source=source, ) @@ -244,8 +255,8 @@ def load_codex_cli_credential() -> CodexCliCredential | None: if not isinstance(account_id, str): logger.debug("Codex CLI credentials file has a non-string account_id; using no account") account_id = "" - if not access_token: - logger.debug("Codex CLI credentials file exists but no token found") + if not isinstance(access_token, str) or not access_token: + logger.debug("Codex CLI credentials file exists but no string token found") return None logger.info("Loaded Codex CLI credential") diff --git a/backend/tests/test_credential_loader.py b/backend/tests/test_credential_loader.py index e51b177ad9..7fb1072b89 100644 --- a/backend/tests/test_credential_loader.py +++ b/backend/tests/test_credential_loader.py @@ -322,6 +322,91 @@ def test_load_claude_code_credential_ignores_non_numeric_expires_at(tmp_path, mo assert load_claude_code_credential() is None +@pytest.mark.parametrize( + "access_token", + [ + 12345, + {"token": "sk-ant-oat01-nested"}, + ["sk-ant-oat01-list"], + ], +) +def test_load_claude_code_credential_ignores_non_string_access_token(tmp_path, monkeypatch, access_token): + _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": access_token, "expiresAt": 4_102_444_800_000}}), + encoding="utf-8", + ) + monkeypatch.setenv("CLAUDE_CODE_CREDENTIALS_PATH", str(cred_file)) + + assert load_claude_code_credential() is None + + +@pytest.mark.parametrize("access_token", [" ", "\t\n", " " * 12]) +def test_load_claude_code_credential_ignores_blank_string_access_token(tmp_path, monkeypatch, access_token): + _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": access_token, "expiresAt": 4_102_444_800_000}}), + 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_strips_a_padded_access_token(tmp_path, monkeypatch): + _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-padded \n", "expiresAt": 4_102_444_800_000}}), + encoding="utf-8", + ) + monkeypatch.setenv("CLAUDE_CODE_CREDENTIALS_PATH", str(cred_file)) + + cred = load_claude_code_credential() + + assert cred is not None + assert cred.access_token == "sk-ant-oat01-padded" + + +@pytest.mark.parametrize( + "refresh_token", + [ + 12345, + None, + {"token": "sk-ant-ort01-nested"}, + ["sk-ant-ort01-list"], + ], +) +def test_load_claude_code_credential_degrades_non_string_refresh_token(tmp_path, monkeypatch, refresh_token): + _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", + "refreshToken": refresh_token, + "expiresAt": 4_102_444_800_000, + } + } + ), + encoding="utf-8", + ) + monkeypatch.setenv("CLAUDE_CODE_CREDENTIALS_PATH", str(cred_file)) + + cred = load_claude_code_credential() + + assert cred is not None + assert cred.access_token == "sk-ant-oat01-test" + assert cred.refresh_token == "" + + 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)) @@ -356,6 +441,40 @@ def test_load_claude_code_credential_falls_back_to_default_when_override_expires assert cred.source == "claude-cli-file" +def test_load_claude_code_credential_falls_back_to_default_when_override_access_token_is_non_string(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": 12345, "expiresAt": 4_102_444_800_000}}), + 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( @@ -448,3 +567,21 @@ def test_load_codex_cli_credential_ignores_non_string_account_id(tmp_path, monke assert cred is not None assert cred.access_token == "codex-access-token" assert cred.account_id == "" + + +@pytest.mark.parametrize("access_token", [999, {"nested": "token"}, ["token-list"]]) +def test_load_codex_cli_credential_ignores_non_string_access_token(tmp_path, monkeypatch, access_token): + auth_path = tmp_path / "auth.json" + auth_path.write_text(json.dumps({"access_token": access_token, "account_id": "acct-1"})) + monkeypatch.setenv("CODEX_AUTH_PATH", str(auth_path)) + + assert load_codex_cli_credential() is None + + +def test_codex_chat_model_reports_missing_credential_for_non_string_access_token(tmp_path, monkeypatch): + auth_path = tmp_path / "auth.json" + auth_path.write_text(json.dumps({"access_token": 999, "account_id": "acct-1"})) + monkeypatch.setenv("CODEX_AUTH_PATH", str(auth_path)) + + with pytest.raises(ValueError, match="Codex CLI credential not found"): + CodexChatModel(model="gpt-5.4")