Skip to content

fix(models): skip Claude credentials sources with a non-numeric expiresAt - #5591

Merged
WillemJiang merged 1 commit into
bytedance:mainfrom
BlueX888:fix/prep-claude-credential-nonnumeric-expiresat-typeerror
Sep 20, 2026
Merged

WillemJiang merged 1 commit into
bytedance:mainfrom
BlueX888:fix/prep-claude-credential-nonnumeric-expiresat-typeerror

Conversation

@BlueX888

Copy link
Copy Markdown
Contributor

Fixes #5589

Why

_extract_claude_code_credential copies claudeAiOauth.expiresAt straight into ClaudeCodeCredential.expires_at with no type check — backend/packages/harness/deerflow/models/credential_loader.py:166 on main:

expires_at=oauth.get("expiresAt", 0),

ClaudeCodeCredential.is_expired (line 58) then evaluates if self.expires_at <= 0:. When the credentials file holds "expiresAt": "1773430695128" (a string), or null, a list, or an object, that comparison raises

TypeError: '<=' not supported between instances of 'str' and 'int'

out of load_claude_code_credential(). The exception is not caught, so the lookup loop never advances to the next candidate source — a credentials file at $CLAUDE_CODE_CREDENTIALS_PATH with a bad expiresAt prevents the loader from ever reaching ~/.claude/.credentials.json. It also propagates through ClaudeChatModel.model_post_init, which calls the loader without a guard, so one such file makes every ClaudeChatModel construction raise instead of degrading.

This is inconsistent with the rest of the function. A missing expiresAt falls back to the dataclass default 0, which is_expired explicitly tolerates (if self.expires_at <= 0: return False), while an explicit null — the same "expiry unknown" — aborts the load. The sibling guard for a malformed claudeAiOauth container (merged in #5494 / bec423188) established the contract for this function: a source that cannot be used must be skipped with a debug log so the loader falls through to the next one. expiresAt was the last unvalidated input in it.

What changed

A credentials source whose claudeAiOauth.expiresAt is not a number is now skipped with a debug log, in the same shape as the two neighbouring guards in the same function:

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

load_claude_code_credential() no longer raises for such a file: it returns a credential from a later source in the documented lookup order, or None. Files with a numeric expiresAt, and files with no expiresAt at all, behave exactly as before — a missing key still yields the dataclass default 0 and is not treated as expired.

Surface area

None of the listed boxes apply — the change is confined to the harness credential loader (backend/packages/harness/deerflow/models/credential_loader.py), which is not an endpoint under backend/app, an agent node/graph/prompt change, or a sandbox/skills/dependency change.

  • Frontend UI
  • Backend API
  • Agents / LangGraph
  • Sandbox
  • Skills
  • Dependencies
  • Default behavior change
  • Docs / tests / CI only

Bug fix verification

  • Test paths that reproduce the bug:
    • backend/tests/test_credential_loader.py::test_load_claude_code_credential_ignores_non_numeric_expires_at (parametrized over "1773430695128", None, [], {})
    • backend/tests/test_credential_loader.py::test_load_claude_code_credential_falls_back_to_default_when_override_expires_at_is_non_numeric
  • Did it go red on main and green on this branch? yes

Red on main (tests applied, source fix reverted) — cd backend && PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run --locked pytest tests/test_credential_loader.py -k non_numeric -q:

packages/harness/deerflow/models/credential_loader.py:170: in _extract_claude_code_credential
    if cred.is_expired:
       ^^^^^^^^^^^^^^^
self = ClaudeCodeCredential(access_token='sk-ant-oat01-override', refresh_token='', expires_at='1773430695128', source='claude-cli-file')

    @property
    def is_expired(self) -> bool:
>       if self.expires_at <= 0:
           ^^^^^^^^^^^^^^^^^^^^
E       TypeError: '<=' not supported between instances of 'str' and 'int'

packages/harness/deerflow/models/credential_loader.py:58: TypeError
============================ short test summary info ============================
FAILED tests/test_credential_loader.py::test_load_claude_code_credential_ignores_non_numeric_expires_at[1773430695128]
FAILED tests/test_credential_loader.py::test_load_claude_code_credential_ignores_non_numeric_expires_at[None]
FAILED tests/test_credential_loader.py::test_load_claude_code_credential_ignores_non_numeric_expires_at[expires_at2]
FAILED tests/test_credential_loader.py::test_load_claude_code_credential_ignores_non_numeric_expires_at[expires_at3]
FAILED tests/test_credential_loader.py::test_load_claude_code_credential_falls_back_to_default_when_override_expires_at_is_non_numeric
5 failed, 19 deselected in 1.12s

Green on this branch, same command:

5 passed, 19 deselected in 1.06s

Validation

cd backend && uv run ruff check .
All checks passed!

cd backend && uv run ruff format --check .
1545 files already formatted

cd backend && PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run --locked pytest tests/test_credential_loader.py -q
24 passed in 1.33s

cd backend && PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run --locked pytest -m "not live" --ignore=tests/blocking_io tests/ -q
17552 passed, 199 skipped, 3 deselected, 32 warnings in 444.91s (0:07:24)

AI assistance

Tool(s) used: Claude Code

How you used it: Claude Code drafted the change, the regression tests and the commit message. I ran the red/green cycle by reverting the source fix and re-running the new tests on main, then on this branch, ran the full backend suite and ruff on the branch, and reviewed the final diff before opening this PR.

  • I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

…esAt

`_extract_claude_code_credential` copied `expiresAt` straight into
`ClaudeCodeCredential.expires_at`, so a credentials file whose `expiresAt` is a
string, null, list or object reached `is_expired` and raised
`TypeError: '<=' not supported between instances of 'str' and 'int'`. That
aborted the whole lookup instead of skipping the malformed source and moving on
down the documented order, the way the rest of the loader already behaves for a
malformed `claudeAiOauth` container.

Validate the field the way the sibling branches validate their input: log a
debug line and skip the source so the next candidate is tried.
@github-actions github-actions Bot added area:backend Gateway / runtime / core backend under backend/ risk:medium Medium risk: regular code changes size/S PR changes 20-100 lines labels Sep 19, 2026
@WillemJiang WillemJiang added this to the 2.1.0 milestone Sep 20, 2026
@WillemJiang
WillemJiang merged commit 5051709 into bytedance:main Sep 20, 2026
16 checks passed
@WillemJiang WillemJiang modified the milestones: 2.1.0, 2.2.0 Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:backend Gateway / runtime / core backend under backend/ risk:medium Medium risk: regular code changes size/S PR changes 20-100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Claude credential loader raises TypeError instead of skipping the source when claudeAiOauth.expiresAt is a string or null

2 participants