mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-03 06:12:14 +08:00
* 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>
167 lines
5.3 KiB
Python
Executable File
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())
|