refactor(agent/anthropic_credentials,account_usage,azure_identity_adapter): fold single-use branches, compact docstrings
This commit is contained in:
@@ -64,8 +64,7 @@ def _parse_dt(value: Any) -> Optional[datetime]:
|
||||
return datetime.fromtimestamp(float(value), tz=timezone.utc)
|
||||
if not isinstance(value, str) or not (text := value.strip()):
|
||||
return None
|
||||
if text.endswith("Z"):
|
||||
text = text[:-1] + "+00:00"
|
||||
text = text[:-1] + "+00:00" if text.endswith("Z") else text
|
||||
try:
|
||||
dt = datetime.fromisoformat(text)
|
||||
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
||||
@@ -193,12 +192,9 @@ def _fetch_portal_account(timeout: float):
|
||||
|
||||
|
||||
def nous_credits_lines(*, markdown: bool = False, timeout: float = 10.0) -> list[str]:
|
||||
"""Rendered Nous-credits /usage lines, or [] when there's nothing to show.
|
||||
|
||||
Independent of any live agent (logged-in gate, then a bounded portal fetch); shared by CLI ``_show_usage``
|
||||
and the TUI ``session.usage`` RPC. Fail-open: any hiccup or timeout → []. HERMES_DEV_CREDITS_FIXTURE
|
||||
renders from the fixture instead of the portal.
|
||||
"""
|
||||
"""Rendered Nous-credits /usage lines, or [] when there's nothing to show. Independent of any live agent
|
||||
(logged-in gate, then a bounded portal fetch); shared by CLI ``_show_usage`` and the TUI ``session.usage`` RPC.
|
||||
Fail-open: any hiccup or timeout → []. HERMES_DEV_CREDITS_FIXTURE renders from the fixture instead of the portal."""
|
||||
try:
|
||||
from agent.credits_tracker import dev_fixture_credits_state
|
||||
fixture = dev_fixture_credits_state()
|
||||
@@ -333,10 +329,8 @@ def _codex_banked_resets(payload: dict) -> int:
|
||||
|
||||
|
||||
def _codex_headers(token: str, account_id: Optional[str]) -> dict[str, str]:
|
||||
headers = {"Authorization": f"Bearer {token}", "Accept": "application/json", "User-Agent": "codex-cli"}
|
||||
if account_id:
|
||||
headers["ChatGPT-Account-Id"] = account_id
|
||||
return headers
|
||||
return {"Authorization": f"Bearer {token}", "Accept": "application/json", "User-Agent": "codex-cli",
|
||||
**({"ChatGPT-Account-Id": account_id} if account_id else {})}
|
||||
|
||||
|
||||
def _get_json(url: str, headers: dict[str, str], *, timeout: float) -> dict:
|
||||
@@ -541,15 +535,11 @@ def _fetch_openrouter_account_usage(base_url: Optional[str], api_key: Optional[s
|
||||
balance = float(credits.get("total_credits") or 0.0) - float(credits.get("total_usage") or 0.0)
|
||||
details = [f"Credits balance: ${max(0.0, balance):.2f}"]
|
||||
windows: list[AccountUsageWindow] = []
|
||||
limit = key_data.get("limit")
|
||||
limit_remaining = key_data.get("limit_remaining")
|
||||
limit, limit_remaining, usage = key_data.get("limit"), key_data.get("limit_remaining"), key_data.get("usage")
|
||||
limit_reset = str(key_data.get("limit_reset") or "").strip()
|
||||
usage = key_data.get("usage")
|
||||
if _is_num(limit) and float(limit) > 0 and _is_num(limit_remaining) and 0 <= float(limit_remaining) <= float(limit):
|
||||
limit_value, remaining_value = float(limit), float(limit_remaining)
|
||||
detail_parts = [f"${remaining_value:.2f} of ${limit_value:.2f} remaining"]
|
||||
if limit_reset:
|
||||
detail_parts.append(f"resets {limit_reset}")
|
||||
detail_parts = [f"${remaining_value:.2f} of ${limit_value:.2f} remaining", *([f"resets {limit_reset}"] if limit_reset else [])]
|
||||
windows.append(AccountUsageWindow(label="API key quota", used_percent=((limit_value - remaining_value) / limit_value) * 100,
|
||||
detail=" • ".join(detail_parts)))
|
||||
if _is_num(usage):
|
||||
|
||||
@@ -64,11 +64,8 @@ def _is_oauth_token(key: str) -> bool:
|
||||
|
||||
|
||||
class CredentialPersistError(RuntimeError):
|
||||
"""A rotated single-use credential could not be durably committed.
|
||||
|
||||
The refresh POST already spent the old refresh token, so a swallowed write failure leaves a consumed
|
||||
pair on disk that later replays as invalid_grant.
|
||||
"""
|
||||
"""A rotated single-use credential could not be durably committed. The refresh POST already spent the old
|
||||
refresh token, so a swallowed write failure leaves a consumed pair on disk that later replays as invalid_grant."""
|
||||
|
||||
def __init__(self, path: Any, cause: BaseException) -> None:
|
||||
super().__init__(f"failed to durably persist rotated Anthropic credentials to {path}: {cause}")
|
||||
@@ -87,12 +84,9 @@ def _load_json_if_exists(path: Path, what: str) -> Optional[Any]:
|
||||
|
||||
|
||||
def _atomic_write_private_json(path: Path, payload: Any) -> None:
|
||||
"""Write *payload* via a 0o600 O_EXCL temp file + fsync + os.replace.
|
||||
|
||||
The token is never briefly umask-readable (write_text + chmod had a TOCTOU window); the random suffix
|
||||
avoids collisions with concurrent writers and crashed leftovers. The parent dir's mode is left alone
|
||||
(~/.claude/ is owned by Claude Code).
|
||||
"""
|
||||
"""Write *payload* via a 0o600 O_EXCL temp file + fsync + os.replace: the token is never briefly umask-readable
|
||||
(write_text + chmod had a TOCTOU window); the random suffix avoids collisions with concurrent writers and
|
||||
crashed leftovers. The parent dir's mode is left alone (~/.claude/ is owned by Claude Code)."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(f".tmp.{os.getpid()}.{secrets.token_hex(4)}")
|
||||
try:
|
||||
@@ -238,14 +232,11 @@ def _read_claude_code_credentials_from_keychain() -> Optional[Dict[str, Any]]:
|
||||
logger.debug("Keychain: no entry found for 'Claude Code-credentials'")
|
||||
return None
|
||||
raw = result.stdout.strip()
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
return _claude_oauth_record(json.loads(raw), "macos_keychain") if raw else None
|
||||
except json.JSONDecodeError:
|
||||
logger.debug("Keychain: credentials payload is not valid JSON")
|
||||
return None
|
||||
return _claude_oauth_record(data, "macos_keychain")
|
||||
|
||||
|
||||
def claude_code_credentials_path() -> Path:
|
||||
@@ -259,12 +250,9 @@ def _read_claude_code_credentials_from_file() -> Optional[Dict[str, Any]]:
|
||||
|
||||
|
||||
def read_claude_code_credentials() -> Optional[Dict[str, Any]]:
|
||||
"""Read refreshable Claude Code OAuth credentials (Keychain and/or file).
|
||||
|
||||
When both exist: prefer the only non-expired one (Claude Code 2.1.x refreshes one source but not the
|
||||
other), else the later ``expiresAt`` so a refresh uses the freshest refreshToken. ~/.claude.json
|
||||
primaryApiKey is deliberately excluded.
|
||||
"""
|
||||
"""Read refreshable Claude Code OAuth credentials (Keychain and/or file). When both exist: prefer the only
|
||||
non-expired one (Claude Code 2.1.x refreshes one source but not the other), else the later ``expiresAt`` so a
|
||||
refresh uses the freshest refreshToken. ~/.claude.json primaryApiKey is deliberately excluded."""
|
||||
kc_creds = _read_claude_code_credentials_from_keychain()
|
||||
file_creds = _read_claude_code_credentials_from_file()
|
||||
if not (kc_creds and file_creds):
|
||||
@@ -278,9 +266,7 @@ def read_claude_code_credentials() -> Optional[Dict[str, Any]]:
|
||||
def is_claude_code_token_valid(creds: Dict[str, Any]) -> bool:
|
||||
"""Non-expired access token (60s buffer); no expiresAt means managed key → valid if present."""
|
||||
expires_at = creds.get("expiresAt", 0)
|
||||
if not expires_at:
|
||||
return bool(creds.get("accessToken"))
|
||||
return int(time.time() * 1000) < (expires_at - 60_000)
|
||||
return int(time.time() * 1000) < (expires_at - 60_000) if expires_at else bool(creds.get("accessToken"))
|
||||
|
||||
|
||||
# ── OAuth token endpoint ──
|
||||
@@ -330,13 +316,10 @@ def refresh_anthropic_oauth_pure(refresh_token: str, *, use_json: bool = False)
|
||||
|
||||
|
||||
def _refresh_oauth_token(creds: Dict[str, Any]) -> Optional[str]:
|
||||
"""Refresh an expired Claude Code OAuth token, returning the new access token.
|
||||
|
||||
Refresh tokens are single-use and Claude Code refreshes on its own schedule, so we first re-read the
|
||||
live sources and adopt an already-rotated token instead of racing it into ``invalid_grant``. Read,
|
||||
decision, POST and write-back share the pool's path-keyed cross-process lock (else two profiles can
|
||||
spend one refresh token).
|
||||
"""
|
||||
"""Refresh an expired Claude Code OAuth token, returning the new access token. Refresh tokens are single-use and
|
||||
Claude Code refreshes on its own schedule, so we first re-read the live sources and adopt an already-rotated
|
||||
token instead of racing it into ``invalid_grant``. Read, decision, POST and write-back share the pool's
|
||||
path-keyed cross-process lock (else two profiles can spend one refresh token)."""
|
||||
try:
|
||||
from hermes_cli.auth import AUTH_LOCK_TIMEOUT_SECONDS, _auth_store_lock, env_float
|
||||
refresh_timeout_seconds = env_float("HERMES_ANTHROPIC_REFRESH_TIMEOUT_SECONDS", 20)
|
||||
@@ -392,11 +375,9 @@ def _refresh_oauth_token(creds: Dict[str, Any]) -> Optional[str]:
|
||||
def _write_claude_code_credentials(
|
||||
access_token: str, refresh_token: str, expires_at_ms: int, *, scopes: Optional[list] = None
|
||||
) -> None:
|
||||
"""Commit refreshed credentials to ~/.claude/.credentials.json; ``CredentialPersistError`` on any failure.
|
||||
|
||||
*scopes* (or the previously stored scopes) are persisted because Claude Code >=2.1.81 gates on
|
||||
``"user:inference"`` being present. A corrupt existing file is a persist failure too.
|
||||
"""
|
||||
"""Commit refreshed credentials to ~/.claude/.credentials.json; ``CredentialPersistError`` on any failure (a
|
||||
corrupt existing file included). *scopes* (or the previously stored scopes) are persisted because Claude Code
|
||||
>=2.1.81 gates on ``"user:inference"`` being present."""
|
||||
cred_path = claude_code_credentials_path()
|
||||
try:
|
||||
existing = json.loads(cred_path.read_text(encoding="utf-8")) if cred_path.exists() else {}
|
||||
@@ -429,18 +410,14 @@ def _resolve_claude_code_token_from_credentials(creds: Optional[Dict[str, Any]]
|
||||
return creds["accessToken"]
|
||||
logger.debug("Claude Code credentials expired — attempting refresh")
|
||||
refreshed = _refresh_oauth_token(creds)
|
||||
if refreshed:
|
||||
return refreshed
|
||||
logger.debug("Token refresh failed — re-run 'claude setup-token' to reauthenticate")
|
||||
return None
|
||||
if not refreshed:
|
||||
logger.debug("Token refresh failed — re-run 'claude setup-token' to reauthenticate")
|
||||
return refreshed or None
|
||||
|
||||
|
||||
def _prefer_refreshable_claude_code_token(env_token: str, creds: Optional[Dict[str, Any]]) -> Optional[str]:
|
||||
"""Prefer refreshable Claude Code creds over a static env OAuth token.
|
||||
|
||||
Hermes historically persisted setup tokens into ANTHROPIC_TOKEN; that static token would otherwise
|
||||
win before the refreshable credential file is inspected, making refresh impossible.
|
||||
"""
|
||||
"""Prefer refreshable Claude Code creds over a static env OAuth token: Hermes historically persisted setup tokens
|
||||
into ANTHROPIC_TOKEN, and that static token would otherwise win before the refreshable file is inspected."""
|
||||
if not (env_token and _is_oauth_token(env_token) and isinstance(creds, dict) and creds.get("refreshToken")):
|
||||
return None
|
||||
resolved = _resolve_claude_code_token_from_credentials(creds)
|
||||
@@ -451,12 +428,9 @@ def _prefer_refreshable_claude_code_token(env_token: str, creds: Optional[Dict[s
|
||||
|
||||
|
||||
def _resolve_anthropic_pool_token() -> Optional[str]:
|
||||
"""First available Anthropic OAuth token from credential_pool, read-only.
|
||||
|
||||
Enumerates with ``clear_expired=False, refresh=False`` (never ``select()``) so diagnostic call sites
|
||||
(account_usage, ``hermes models``) never mutate auth.json or hit the network; refresh-on-expiry
|
||||
belongs to the API call path's pool recovery.
|
||||
"""
|
||||
"""First available Anthropic OAuth token from credential_pool, read-only: enumerates with ``clear_expired=False,
|
||||
refresh=False`` (never ``select()``) so diagnostic call sites (account_usage, ``hermes models``) never mutate
|
||||
auth.json or hit the network; refresh-on-expiry belongs to the API call path's pool recovery."""
|
||||
try:
|
||||
from agent.credential_pool import AUTH_TYPE_OAUTH, load_pool
|
||||
entries, _pending = load_pool("anthropic")._available_entries(clear_expired=False, refresh=False)
|
||||
@@ -523,9 +497,7 @@ def _root_hermes_oauth_file() -> Optional[Path]:
|
||||
try:
|
||||
from hermes_constants import get_default_hermes_root
|
||||
root = get_default_hermes_root()
|
||||
if root.resolve(strict=False) == get_hermes_home().resolve(strict=False):
|
||||
return None
|
||||
return root / ".anthropic_oauth.json"
|
||||
return None if root.resolve(strict=False) == get_hermes_home().resolve(strict=False) else root / ".anthropic_oauth.json"
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -602,11 +574,9 @@ def _write_hermes_oauth_credentials(
|
||||
access_token: str, refresh_token: Optional[str], expires_at_ms: Optional[int], *, target: Optional[Path] = None
|
||||
) -> None:
|
||||
"""Commit refreshed hermes_pkce tokens to ~/.hermes/.anthropic_oauth.json (``CredentialPersistError`` on failure).
|
||||
|
||||
``target`` lets a named profile commit a grant it BORROWED from the global root back to the ROOT
|
||||
singleton instead of forking a copy under its own HERMES_HOME; without this write-through the next
|
||||
``load_pool()`` re-seeds the stale (consumed) pair from the file over the rotated pool entry.
|
||||
"""
|
||||
``target`` lets a named profile commit a grant it BORROWED from the global root back to the ROOT singleton
|
||||
instead of forking a copy under its own HERMES_HOME; without this write-through the next ``load_pool()``
|
||||
re-seeds the stale (consumed) pair from the file over the rotated pool entry."""
|
||||
_commit_private_json(
|
||||
target if target is not None else _get_hermes_oauth_file(),
|
||||
{"accessToken": access_token, "refreshToken": refresh_token, "expiresAt": expires_at_ms},
|
||||
|
||||
@@ -203,8 +203,8 @@ def describe_active_credential(config: Optional[EntraIdentityConfig] = None, *,
|
||||
return info
|
||||
config = _resolve_config(config, scope, **overrides)
|
||||
info["scope"] = config.scope
|
||||
if _env("AZURE_TENANT_ID"):
|
||||
info["tenant_id_env"] = _env("AZURE_TENANT_ID")
|
||||
if tenant := _env("AZURE_TENANT_ID"):
|
||||
info["tenant_id_env"] = tenant
|
||||
info["env_sources"] = [label for label, present in _ENV_SOURCE_CHECKS if present()]
|
||||
result = _probe_token(config, timeout_seconds)
|
||||
if result is None:
|
||||
|
||||
Reference in New Issue
Block a user