cf60ebbdfd264c3fc067b9e3230df8752c50d2ec
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dd522d0f77 |
refactor(agent/creds): unify Anthropic credential I/O and rate-limit header helpers (-30% LOC)
- anthropic_credentials: _load_json_if_exists / _claude_oauth_record / _atomic_write_private_json / _post_oauth_token / _oauth_token_state shared by the Claude Code + hermes_pkce read/write/refresh/exchange paths; _fingerprint delegates to credential_persistence.fingerprint_secret_value. - rate_limit_tracker: lower_headers / has_rate_limit_headers exported and reused by nous_rate_guard (header parsing, bucket exhaustion via _is_exhausted); format_remaining = _fmt_seconds alias (public name kept). - _safe_int stays tolerant of non-finite values (parity with origin). - Docstring/comment compaction; every WHY kept. - Parity: /tmp/rf/creds_c_parity_probe.py output identical to origin baseline (except wall-clock 'captured N ago' text); refresh_anthropic_oauth_pure old-vs-new on 6 token payloads identical. |
||
|
|
3038493ee6 |
fix(auth): never fork single-use OAuth grants across profiles (#100339)
Anthropic / Codex / xAI OAuth refresh tokens are single-use: a grant copied into a second auth.json is one credential with two owners, and the first profile to refresh it revokes the pair for every sibling (invalid_grant / refresh_token_reused). Two code paths forked grants that way: 1. `hermes profile create --clone-all` and the dashboard/TUI `mirror_credentials` flow copied auth.json (+ .anthropic_oauth.json) verbatim. Both now run `strip_cloned_single_use_oauth_grants()`, which drops OAuth rows for SINGLE_USE_REFRESH_POOL_PROVIDERS, the matching `providers.<id>` device-code blocks, and the PKCE singleton file; API keys are still copied. The clone reads the root grant through the existing credential-pool root fallback. 2. A named profile with no local rows BORROWS the root grant via `read_credential_pool()`'s fallback, but every persist (`CredentialPool._persist`, `load_pool` reseed, `remove_index`) wrote the rows into the profile's own auth.json — materializing a fork on the first rotation. `persist_pool_entries()` now routes borrowed single-use rows back to the root store (update-only, under the root lock; never falls back to a local copy). A borrowed `hermes_pkce` rotation commits its singleton to the root `.anthropic_oauth.json`, the borrower never prunes root-seeded rows it cannot see the backing file for, and `hermes -p <profile> auth add` persists only the profile's own rows. Live repro (real imports, temp root + profiles, fake single-use token endpoint): before — first profile rotation RT0->RT1 in profile only; root and sibling then hit `invalid_grant`, `resolve_anthropic_token()` -> None. After — rotation lands in root; root and both siblings select AT1, no reuse. Direction per Teknium: stop cloning OAuth into profiles (ONE grant at root, children inherit via context) rather than making clones survive. Supersedes the clone-strip/root-write-through half of #100389 and the init-refresh idea in #100703 (an expired-but-refreshable row already refreshes on select()). Closes #100339 Co-authored-by: HexLab98 <liruixinch@outlook.com> |
||
|
|
b4403a942a |
fix(auth): carry the spent-rotation verdict across processes via a durable sidecar registry
The consumed-but-uncommitted rotation verdict was process-local (_SPENT_ROTATION_FINGERPRINTS), while the credential it protects is explicitly cross-process: ~/.claude/.credentials.json is shared by every Hermes profile and process. A fresh interpreter could lease the stale access token or re-POST the already-spent single-use refresh token and burn the credential family into invalid_grant. - Persist non-secret one-way fingerprints to a sidecar registry next to the shared singleton source (claude_code / hermes_pkce), written under the same path-keyed cross-process lock that serializes refreshes. - Consult the sidecar in the pool resolver, the pool refresh path, and the direct claude_code resolver/refresh before leasing or POSTing. - Two-process regression: A rotates and loses the commit; B (fresh interpreter, empty local registry) must neither lease the stale pair nor POST the spent refresh token. Plus a no-verdict control. Closes the remaining P1 from the exact-head review of f228439b on PR #87891. |
||
|
|
b7a9db8b9a |
fix(auth): keep the borrowed claude_code row out of token authority and carry the spent-rotation verdict through resolution
Two runtime blockers from the exact-head review of c057ef5.
1. A sanitized `claude_code` pool row was treated as token authority.
`claude_code` is a borrowed source: it is absent from the owned-source
allowlist, so `sanitize_borrowed_credential_payload` strips `access_token`
and `refresh_token` before the row reaches `auth.json`. `load_pool()`
re-hydrates the live pair from the singleton on every load, which is what
makes `~/.claude/.credentials.json` — not the pool store — authoritative
for this source.
`_sync_anthropic_entry_from_pool_store()` re-read that persisted row during
refresh. Being token-less, it "differed" from the live entry, so it was
adopted as a rotation performed by another process: `_refresh_entry()`
replaced a usable credential with an empty one and returned it before
`_claude_code_credentials_lock()` and the authoritative re-read were ever
entered. The empty OAuth entry then stayed selectable, because the
empty-runtime-key guard in `_available_entries()` covered API-key rows only.
Repairs: the pool-store sync refuses borrowed sources outright (plus a
defensive refusal of any token-less row, for future sources that sanitize on
write); the `claude_code` branch of `_refresh_entry()` now runs before the
generic adopt-and-return shortcut, so the path-keyed lock and the
authoritative re-read are always entered before deciding to POST or adopt;
and an OAuth entry with no access token is never leased.
2. A failed commit still fell through to the same spent credential.
`_refresh_oauth_token()` correctly returns None when the refresh POST
rotated the single-use token but the replacement could not be committed.
That verdict did not survive the caller: `resolve_anthropic_token()`
continued to `_resolve_anthropic_pool_token()`, which enumerates read-only
(`clear_expired=False, refresh=False`) over a pool that `load_pool()` had
just re-seeded from the unchanged singleton — so the pair whose refresh half
was already spent came back as a healthy token, and
`_refresh_provider_credentials("anthropic")` reported success and evicted
its cached clients.
Repair: every commit-failure path records the consumed pre-rotation pair as
non-reversible fingerprints (bounded, process-local), and both the Claude
Code file resolver and the pool resolver refuse a credential whose
fingerprint is on that list. `_refresh_provider_credentials("anthropic")`
consequently returns False when the spent family is the only credential,
while genuinely independent pool credentials stay eligible.
Coverage: `test_anthropic_borrowed_row_authority.py` starts from
`load_pool()` reading an actually persisted, actually sanitized row, forces
a refresh, and asserts the full pair survives with exactly one POST and one
commit, that the shared-file lock is entered, and that no empty OAuth entry
can be leased. `test_anthropic_spent_rotation_verdict.py` takes the full
resolver path: successful POST plus failed commit must make
`resolve_anthropic_token()` return None, make
`_refresh_provider_credentials("anthropic")` return False, and keep the
spent fingerprint out of every lease — with a control proving a successful
commit quarantines nothing and an independent credential still resolving.
Five of the seven new borrowed-row tests fail on the previous head, and the
three resolution tests fail with the verdict disabled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gcoy6nLTg5R6FHHhjcLZEC
|
||
|
|
7cbffdd125 |
refactor(anthropic): split the adapter godfile into four modules
`agent/anthropic_adapter.py` was 3,423 lines and this PR adds another auth boundary to it. Split along the seams that were already there, so the credential surface this PR changes has a single owner instead of being interleaved with request building: - `agent/anthropic_endpoints.py` (258) — base-URL/endpoint-family predicates. Pure functions over a URL string, which is what lets both of the modules below depend on it without a cycle. - `agent/anthropic_message_convert.py` (1,225) — OpenAI-style to Anthropic Messages payload conversion: model ids, tool schemas, content/thinking blocks, tool_use pairing, cache_control, screenshot eviction, blank-block scrubbing. - `agent/anthropic_credentials.py` (910) — credential sources, the OAuth flows, and the refresh commit (`CredentialPersistError` and both singleton writers). - `agent/anthropic_adapter.py` (1,215) — client construction and the Messages API call, re-exporting every name from the three modules above so existing `from agent.anthropic_adapter import ...` imports keep resolving. The re-export surface was diffed against the pre-split module: nothing dropped. Call sites that read a moved name through the adapter's namespace at runtime (`credential_pool._refresh_entry_impl`, `auxiliary_client`) now import it from the defining module, so there is one patchable seam rather than two bindings that can disagree. The tests that monkeypatched those seams were retargeted to match; no assertion was changed. No behavior change. |