f0d27863c2
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>
77 lines
2.3 KiB
Python
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",
|
|
]
|