Files
bot-bottle/bot_bottle/contrib/claude/claude_auth.py
T
didericis-claude f0d27863c2
lint / lint (push) Successful in 2m19s
test / unit (pull_request) Successful in 1m2s
test / integration (pull_request) Successful in 22s
test / coverage (pull_request) Successful in 1m14s
feat(claude): add forward_host_credentials support
Reads the host's Claude OAuth session key from ~/.claude.json at launch
and forwards it only to the egress sidecar (never to the agent), placing
a placeholder CLAUDE_CODE_OAUTH_TOKEN in the agent env so Claude Code
starts without seeing the real credential.

Mirrors the existing Codex forward_host_credentials flow (PRD 0029).
Adds claude_auth.py to extract and validate the sessionKey, a
CLAUDE_HOST_CREDENTIAL_TOKEN_REF constant in egress.py, and updates
manifest_agent.py to allow the flag for both 'codex' and 'claude'
templates. Also adds a mutual-exclusion check that rejects setting
both auth_token and forward_host_credentials together.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 21:14:37 +00:00

77 lines
2.3 KiB
Python

"""Host Claude auth helpers.
Reads the host's Claude Code auth state and returns only the
session key needed by egress. Does not expose refresh tokens
or raw auth payloads.
"""
from __future__ import annotations
import json
import os
from datetime import datetime, timezone
from pathlib import Path
from ...log import die
def claude_auth_path(host_env: dict[str, str] | None = None) -> Path:
env = os.environ if host_env is None else host_env
home = env.get("HOME")
if home:
return Path(home) / ".claude.json"
return Path.home() / ".claude.json"
def claude_host_access_token(
host_env: dict[str, str] | None = None,
*,
now: datetime | None = None,
) -> str:
path = claude_auth_path(host_env)
if not path.is_file():
die(
f"claude host credentials: auth file missing at {path}. "
"Run `claude login` on the host or disable "
"agent_provider.forward_host_credentials."
)
try:
raw = json.loads(path.read_text())
except (OSError, json.JSONDecodeError) as e:
die(f"claude host credentials: could not read valid JSON at {path}: {e}")
if not isinstance(raw, dict):
die(f"claude host credentials: {path} must contain a JSON object")
oauth = raw.get("oauthAccount")
if not isinstance(oauth, dict):
die(
f"claude host credentials: {path} is missing oauthAccount. "
"Run `claude login` on the host or disable "
"agent_provider.forward_host_credentials."
)
session_key = oauth.get("sessionKey")
if not isinstance(session_key, str) or not session_key:
die(
f"claude host credentials: {path} oauthAccount.sessionKey is missing "
"or empty. Run `claude login` on the host and restart the bottle."
)
expires_at = oauth.get("expiresAt")
if isinstance(expires_at, (int, float)):
check_now = now or datetime.now(timezone.utc)
exp_dt = datetime.fromtimestamp(float(expires_at), timezone.utc)
if exp_dt <= check_now:
die(
"claude host credentials: host Claude session token is expired. "
"Run `claude login` on the host and restart the bottle."
)
return session_key
__all__ = [
"claude_auth_path",
"claude_host_access_token",
]