Files
deer-flow/scripts/export_claude_code_oauth.py
Purricane 835ba041f8 feat: add Claude Code OAuth and Codex CLI as LLM providers (#1166)
* feat: add Claude Code OAuth and Codex CLI providers

Port of bytedance/deer-flow#1136 from @solanian's feat/cli-oauth-providers branch.\n\nCarries the feature forward on top of current main without the original CLA-blocked commit metadata, while preserving attribution in the commit message for review.

* fix: harden CLI credential loading

Align Codex auth loading with the current ~/.codex/auth.json shape, make Docker credential mounts directory-based to avoid broken file binds on hosts without exported credential files, and add focused loader tests.

* refactor: tighten codex auth typing

Replace the temporary Any return type in CodexChatModel._load_codex_auth with the concrete CodexCliCredential type after the credential loader was stabilized.

* fix: load Claude Code OAuth from Keychain

Match Claude Code's macOS storage strategy more closely by checking the Keychain-backed credentials store before falling back to ~/.claude/.credentials.json. Keep explicit file overrides and add focused tests for the Keychain path.

* fix: require explicit Claude OAuth handoff

* style: format thread hooks reasoning request

* docs: document CLI-backed auth providers

* fix: address provider review feedback

* fix: harden provider edge cases

* Fix deferred tools, Codex message normalization, and local sandbox paths

* chore: narrow PR scope to OAuth providers

* chore: remove unrelated frontend changes

* chore: reapply OAuth branch frontend scope cleanup

* fix: preserve upload guards with reasoning effort wiring

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-03-22 22:39:50 +08:00

167 lines
5.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Export Claude Code OAuth credentials from macOS Keychain on purpose.
This helper is intentionally manual. DeerFlow runtime does not probe Keychain.
Use this script when you want to bridge an existing Claude Code login into an
environment variable or an exported credentials file for DeerFlow.
"""
from __future__ import annotations
import argparse
import json
import os
import platform
import shlex
import subprocess
import sys
import tempfile
from hashlib import sha256
from pathlib import Path
from typing import Any
def claude_code_oauth_file_suffix() -> str:
if os.getenv("CLAUDE_CODE_CUSTOM_OAUTH_URL"):
return "-custom-oauth"
if os.getenv("USE_LOCAL_OAUTH") or os.getenv("LOCAL_BRIDGE"):
return "-local-oauth"
if os.getenv("USE_STAGING_OAUTH"):
return "-staging-oauth"
return ""
def default_service_name() -> str:
service = f"Claude Code{claude_code_oauth_file_suffix()}-credentials"
config_dir = os.getenv("CLAUDE_CONFIG_DIR")
if config_dir:
config_hash = sha256(str(Path(config_dir).expanduser()).encode()).hexdigest()[:8]
service = f"{service}-{config_hash}"
return service
def default_account_name() -> str:
return os.getenv("USER") or "claude-code-user"
def load_keychain_container(service: str, account: str) -> dict[str, Any]:
if platform.system() != "Darwin":
raise RuntimeError("Claude Code Keychain export is only supported on macOS.")
try:
result = subprocess.run(
["security", "find-generic-password", "-a", account, "-w", "-s", service],
capture_output=True,
text=True,
check=False,
)
except OSError as exc:
raise RuntimeError(f"Failed to invoke macOS security tool: {exc}") from exc
if result.returncode != 0:
stderr = (result.stderr or "").strip() or "unknown Keychain error"
raise RuntimeError(f"Keychain lookup failed for service={service!r} account={account!r}: {stderr}")
secret = (result.stdout or "").strip()
if not secret:
raise RuntimeError("Keychain item was empty.")
try:
data = json.loads(secret)
except json.JSONDecodeError as exc:
raise RuntimeError("Claude Code Keychain item did not contain valid JSON.") from exc
access_token = data.get("claudeAiOauth", {}).get("accessToken", "")
if not access_token:
raise RuntimeError("Claude Code Keychain item did not contain claudeAiOauth.accessToken.")
return data
def write_credentials_file(output_path: Path, data: dict[str, Any]) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp_name = tempfile.mkstemp(prefix=f"{output_path.name}.", suffix=".tmp", dir=output_path.parent)
try:
with os.fdopen(fd, "w", encoding="utf-8") as fh:
fh.write(json.dumps(data, indent=2) + "\n")
Path(tmp_name).replace(output_path)
except Exception:
Path(tmp_name).unlink(missing_ok=True)
raise
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Manually export Claude Code OAuth credentials from macOS Keychain for DeerFlow.",
)
parser.add_argument(
"--service",
default=default_service_name(),
help="Override the Keychain service name. Defaults to Claude Code's computed service name.",
)
parser.add_argument(
"--account",
default=default_account_name(),
help="Override the Keychain account name. Defaults to the current user.",
)
parser.add_argument(
"--show-target",
action="store_true",
help="Print the resolved Keychain service/account without reading Keychain.",
)
parser.add_argument(
"--print-token",
action="store_true",
help="Print only the OAuth access token to stdout.",
)
parser.add_argument(
"--print-export",
action="store_true",
help="Print a shell export command for CLAUDE_CODE_OAUTH_TOKEN.",
)
parser.add_argument(
"--write-credentials",
type=Path,
help="Write the full Claude credentials container to this file with 0600 permissions.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
if args.show_target:
print(f"service={args.service}")
print(f"account={args.account}")
if not any([args.print_token, args.print_export, args.write_credentials]):
if not args.show_target:
print("No export action selected. Use --show-target, --print-export, --print-token, or --write-credentials.", file=sys.stderr)
return 2
return 0
try:
data = load_keychain_container(service=args.service, account=args.account)
except RuntimeError as exc:
print(str(exc), file=sys.stderr)
return 1
access_token = data["claudeAiOauth"]["accessToken"]
if args.print_token:
print(access_token)
if args.print_export:
print(f"export CLAUDE_CODE_OAUTH_TOKEN={shlex.quote(access_token)}")
if args.write_credentials:
output_path = args.write_credentials.expanduser()
write_credentials_file(output_path, data)
print(f"Wrote Claude Code credentials to {output_path}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())