code-puppy 0.0.214__py3-none-any.whl → 0.0.366__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Configuration for the Antigravity OAuth plugin."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
from code_puppy import config
|
|
7
|
+
|
|
8
|
+
# Antigravity OAuth configuration
|
|
9
|
+
ANTIGRAVITY_OAUTH_CONFIG: Dict[str, Any] = {
|
|
10
|
+
# OAuth endpoints
|
|
11
|
+
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
|
12
|
+
"token_url": "https://oauth2.googleapis.com/token",
|
|
13
|
+
# Callback handling
|
|
14
|
+
"redirect_host": "http://localhost",
|
|
15
|
+
"redirect_path": "oauth-callback",
|
|
16
|
+
"callback_port_range": (51121, 51150),
|
|
17
|
+
"callback_timeout": 180,
|
|
18
|
+
# Model configuration
|
|
19
|
+
"prefix": "antigravity-",
|
|
20
|
+
"default_context_length": 200000,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_token_storage_path() -> Path:
|
|
25
|
+
"""Get the path for storing OAuth tokens."""
|
|
26
|
+
data_dir = Path(config.DATA_DIR)
|
|
27
|
+
data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
28
|
+
return data_dir / "antigravity_oauth.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_accounts_storage_path() -> Path:
|
|
32
|
+
"""Get the path for storing multi-account data."""
|
|
33
|
+
data_dir = Path(config.DATA_DIR)
|
|
34
|
+
data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
35
|
+
return data_dir / "antigravity_accounts.json"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_antigravity_models_path() -> Path:
|
|
39
|
+
"""Get the path to the antigravity_models.json file."""
|
|
40
|
+
data_dir = Path(config.DATA_DIR)
|
|
41
|
+
data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
42
|
+
return data_dir / "antigravity_models.json"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Constants for Antigravity OAuth flows and Cloud Code Assist API integration."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
# OAuth client credentials (from Antigravity/Google IDE)
|
|
6
|
+
ANTIGRAVITY_CLIENT_ID = (
|
|
7
|
+
"1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
|
|
8
|
+
)
|
|
9
|
+
ANTIGRAVITY_CLIENT_SECRET = "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"
|
|
10
|
+
|
|
11
|
+
# OAuth scopes required for Antigravity integrations
|
|
12
|
+
ANTIGRAVITY_SCOPES: List[str] = [
|
|
13
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
14
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
15
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
16
|
+
"https://www.googleapis.com/auth/cclog",
|
|
17
|
+
"https://www.googleapis.com/auth/experimentsandconfigs",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# OAuth redirect URI for local CLI callback server
|
|
21
|
+
ANTIGRAVITY_REDIRECT_URI = "http://localhost:51121/oauth-callback"
|
|
22
|
+
|
|
23
|
+
# API endpoints (in fallback order: daily → autopush → prod)
|
|
24
|
+
ANTIGRAVITY_ENDPOINT_DAILY = "https://daily-cloudcode-pa.sandbox.googleapis.com"
|
|
25
|
+
ANTIGRAVITY_ENDPOINT_AUTOPUSH = "https://autopush-cloudcode-pa.sandbox.googleapis.com"
|
|
26
|
+
ANTIGRAVITY_ENDPOINT_PROD = "https://cloudcode-pa.googleapis.com"
|
|
27
|
+
|
|
28
|
+
ANTIGRAVITY_ENDPOINT_FALLBACKS = [
|
|
29
|
+
ANTIGRAVITY_ENDPOINT_DAILY,
|
|
30
|
+
ANTIGRAVITY_ENDPOINT_AUTOPUSH,
|
|
31
|
+
ANTIGRAVITY_ENDPOINT_PROD,
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# Preferred endpoint order for project discovery
|
|
35
|
+
ANTIGRAVITY_LOAD_ENDPOINTS = [
|
|
36
|
+
ANTIGRAVITY_ENDPOINT_PROD,
|
|
37
|
+
ANTIGRAVITY_ENDPOINT_DAILY,
|
|
38
|
+
ANTIGRAVITY_ENDPOINT_AUTOPUSH,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
# Primary endpoint (daily sandbox)
|
|
42
|
+
ANTIGRAVITY_ENDPOINT = ANTIGRAVITY_ENDPOINT_DAILY
|
|
43
|
+
|
|
44
|
+
# Default project ID fallback
|
|
45
|
+
ANTIGRAVITY_DEFAULT_PROJECT_ID = "rising-fact-p41fc"
|
|
46
|
+
|
|
47
|
+
# Request headers for Antigravity API
|
|
48
|
+
ANTIGRAVITY_HEADERS: Dict[str, str] = {
|
|
49
|
+
"User-Agent": "antigravity/1.11.5 windows/amd64",
|
|
50
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
51
|
+
"Client-Metadata": '{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}',
|
|
52
|
+
"x-goog-api-key": "", # Must be present but empty for Antigravity
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Request headers for Gemini CLI fallback
|
|
56
|
+
GEMINI_CLI_HEADERS: Dict[str, str] = {
|
|
57
|
+
"User-Agent": "google-api-nodejs-client/9.15.1",
|
|
58
|
+
"X-Goog-Api-Client": "gl-node/22.17.0",
|
|
59
|
+
"Client-Metadata": "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Provider identifier
|
|
63
|
+
ANTIGRAVITY_PROVIDER_ID = "google"
|
|
64
|
+
|
|
65
|
+
# Available models with their configurations
|
|
66
|
+
ANTIGRAVITY_MODELS: Dict[str, Dict[str, Any]] = {
|
|
67
|
+
# Gemini models
|
|
68
|
+
"gemini-3-pro-low": {
|
|
69
|
+
"name": "Gemini 3 Pro Low (Antigravity)",
|
|
70
|
+
"family": "gemini",
|
|
71
|
+
"context_length": 1048576,
|
|
72
|
+
"max_output": 65535,
|
|
73
|
+
},
|
|
74
|
+
"gemini-3-pro-high": {
|
|
75
|
+
"name": "Gemini 3 Pro High (Antigravity)",
|
|
76
|
+
"family": "gemini",
|
|
77
|
+
"context_length": 1048576,
|
|
78
|
+
"max_output": 65535,
|
|
79
|
+
},
|
|
80
|
+
"gemini-3-flash": {
|
|
81
|
+
"name": "Gemini 3 Flash (Antigravity)",
|
|
82
|
+
"family": "gemini",
|
|
83
|
+
"context_length": 1048576,
|
|
84
|
+
"max_output": 65536,
|
|
85
|
+
},
|
|
86
|
+
# Claude models (non-thinking)
|
|
87
|
+
"claude-sonnet-4-5": {
|
|
88
|
+
"name": "Claude Sonnet 4.5 (Antigravity)",
|
|
89
|
+
"family": "claude",
|
|
90
|
+
"context_length": 200000,
|
|
91
|
+
"max_output": 64000,
|
|
92
|
+
},
|
|
93
|
+
# Claude thinking models
|
|
94
|
+
"claude-sonnet-4-5-thinking-low": {
|
|
95
|
+
"name": "Claude Sonnet 4.5 Thinking Low (Antigravity)",
|
|
96
|
+
"family": "claude",
|
|
97
|
+
"context_length": 200000,
|
|
98
|
+
"max_output": 64000,
|
|
99
|
+
"thinking_budget": 8192,
|
|
100
|
+
},
|
|
101
|
+
"claude-sonnet-4-5-thinking-medium": {
|
|
102
|
+
"name": "Claude Sonnet 4.5 Thinking Medium (Antigravity)",
|
|
103
|
+
"family": "claude",
|
|
104
|
+
"context_length": 200000,
|
|
105
|
+
"max_output": 64000,
|
|
106
|
+
"thinking_budget": 16384,
|
|
107
|
+
},
|
|
108
|
+
"claude-sonnet-4-5-thinking-high": {
|
|
109
|
+
"name": "Claude Sonnet 4.5 Thinking High (Antigravity)",
|
|
110
|
+
"family": "claude",
|
|
111
|
+
"context_length": 200000,
|
|
112
|
+
"max_output": 64000,
|
|
113
|
+
"thinking_budget": 32768,
|
|
114
|
+
},
|
|
115
|
+
"claude-opus-4-5-thinking-low": {
|
|
116
|
+
"name": "Claude Opus 4.5 Thinking Low (Antigravity)",
|
|
117
|
+
"family": "claude",
|
|
118
|
+
"context_length": 200000,
|
|
119
|
+
"max_output": 64000,
|
|
120
|
+
"thinking_budget": 8192,
|
|
121
|
+
},
|
|
122
|
+
"claude-opus-4-5-thinking-medium": {
|
|
123
|
+
"name": "Claude Opus 4.5 Thinking Medium (Antigravity)",
|
|
124
|
+
"family": "claude",
|
|
125
|
+
"context_length": 200000,
|
|
126
|
+
"max_output": 64000,
|
|
127
|
+
"thinking_budget": 16384,
|
|
128
|
+
},
|
|
129
|
+
"claude-opus-4-5-thinking-high": {
|
|
130
|
+
"name": "Claude Opus 4.5 Thinking High (Antigravity)",
|
|
131
|
+
"family": "claude",
|
|
132
|
+
"context_length": 200000,
|
|
133
|
+
"max_output": 64000,
|
|
134
|
+
"thinking_budget": 32768,
|
|
135
|
+
},
|
|
136
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""Core OAuth flow implementation for Antigravity authentication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import secrets
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
from urllib.parse import urlencode
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
from .constants import (
|
|
18
|
+
ANTIGRAVITY_CLIENT_ID,
|
|
19
|
+
ANTIGRAVITY_CLIENT_SECRET,
|
|
20
|
+
ANTIGRAVITY_ENDPOINT_FALLBACKS,
|
|
21
|
+
ANTIGRAVITY_HEADERS,
|
|
22
|
+
ANTIGRAVITY_LOAD_ENDPOINTS,
|
|
23
|
+
ANTIGRAVITY_SCOPES,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class OAuthContext:
|
|
31
|
+
"""Runtime state for an in-progress OAuth flow."""
|
|
32
|
+
|
|
33
|
+
state: str
|
|
34
|
+
code_verifier: str
|
|
35
|
+
code_challenge: str
|
|
36
|
+
redirect_uri: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class AntigravityAuthorization:
|
|
41
|
+
"""Result returned after constructing an OAuth authorization URL."""
|
|
42
|
+
|
|
43
|
+
url: str
|
|
44
|
+
verifier: str
|
|
45
|
+
project_id: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class TokenExchangeSuccess:
|
|
50
|
+
"""Successful token exchange result."""
|
|
51
|
+
|
|
52
|
+
refresh_token: str
|
|
53
|
+
access_token: str
|
|
54
|
+
expires_at: float # Unix timestamp
|
|
55
|
+
email: Optional[str]
|
|
56
|
+
project_id: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class TokenExchangeFailure:
|
|
61
|
+
"""Failed token exchange result."""
|
|
62
|
+
|
|
63
|
+
error: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
TokenExchangeResult = TokenExchangeSuccess | TokenExchangeFailure
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _urlsafe_b64encode(data: bytes) -> str:
|
|
70
|
+
"""Encode bytes to URL-safe base64 without padding."""
|
|
71
|
+
return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _generate_code_verifier() -> str:
|
|
75
|
+
"""Generate a cryptographically secure code verifier for PKCE."""
|
|
76
|
+
return _urlsafe_b64encode(secrets.token_bytes(64))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _compute_code_challenge(code_verifier: str) -> str:
|
|
80
|
+
"""Compute the S256 code challenge from the verifier."""
|
|
81
|
+
digest = hashlib.sha256(code_verifier.encode("utf-8")).digest()
|
|
82
|
+
return _urlsafe_b64encode(digest)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _encode_state(verifier: str, project_id: str = "") -> str:
|
|
86
|
+
"""Encode OAuth state as URL-safe base64."""
|
|
87
|
+
payload = {"verifier": verifier, "projectId": project_id}
|
|
88
|
+
return (
|
|
89
|
+
base64.urlsafe_b64encode(json.dumps(payload).encode("utf-8"))
|
|
90
|
+
.decode("utf-8")
|
|
91
|
+
.rstrip("=")
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _decode_state(state: str) -> tuple[str, str]:
|
|
96
|
+
"""Decode OAuth state back to verifier and project ID."""
|
|
97
|
+
# Normalize base64 encoding
|
|
98
|
+
normalized = state.replace("-", "+").replace("_", "/")
|
|
99
|
+
padding = (4 - len(normalized) % 4) % 4
|
|
100
|
+
padded = normalized + "=" * padding
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
json_str = base64.b64decode(padded).decode("utf-8")
|
|
104
|
+
parsed = json.loads(json_str)
|
|
105
|
+
|
|
106
|
+
verifier = parsed.get("verifier", "")
|
|
107
|
+
if not isinstance(verifier, str) or not verifier:
|
|
108
|
+
raise ValueError("Missing PKCE verifier in state")
|
|
109
|
+
|
|
110
|
+
project_id = parsed.get("projectId", "")
|
|
111
|
+
if not isinstance(project_id, str):
|
|
112
|
+
project_id = ""
|
|
113
|
+
|
|
114
|
+
return verifier, project_id
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.error("Failed to decode OAuth state: %s", e)
|
|
117
|
+
raise ValueError(f"Invalid OAuth state: {e}") from e
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def prepare_oauth_context() -> OAuthContext:
|
|
121
|
+
"""Create a new OAuth PKCE context."""
|
|
122
|
+
state = secrets.token_urlsafe(32)
|
|
123
|
+
code_verifier = _generate_code_verifier()
|
|
124
|
+
code_challenge = _compute_code_challenge(code_verifier)
|
|
125
|
+
|
|
126
|
+
return OAuthContext(
|
|
127
|
+
state=state,
|
|
128
|
+
code_verifier=code_verifier,
|
|
129
|
+
code_challenge=code_challenge,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def assign_redirect_uri(context: OAuthContext, port: int) -> str:
|
|
134
|
+
"""Assign redirect URI for the given OAuth context."""
|
|
135
|
+
redirect_uri = f"http://localhost:{port}/oauth-callback"
|
|
136
|
+
context.redirect_uri = redirect_uri
|
|
137
|
+
return redirect_uri
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_authorization_url(context: OAuthContext, project_id: str = "") -> str:
|
|
141
|
+
"""Build the Google OAuth authorization URL with PKCE parameters."""
|
|
142
|
+
if not context.redirect_uri:
|
|
143
|
+
raise RuntimeError("Redirect URI has not been assigned")
|
|
144
|
+
|
|
145
|
+
# Encode state with verifier for callback verification
|
|
146
|
+
state = _encode_state(context.code_verifier, project_id)
|
|
147
|
+
|
|
148
|
+
params = {
|
|
149
|
+
"client_id": ANTIGRAVITY_CLIENT_ID,
|
|
150
|
+
"response_type": "code",
|
|
151
|
+
"redirect_uri": context.redirect_uri,
|
|
152
|
+
"scope": " ".join(ANTIGRAVITY_SCOPES),
|
|
153
|
+
"code_challenge": context.code_challenge,
|
|
154
|
+
"code_challenge_method": "S256",
|
|
155
|
+
"state": state,
|
|
156
|
+
"access_type": "offline",
|
|
157
|
+
"prompt": "consent",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return f"https://accounts.google.com/o/oauth2/v2/auth?{urlencode(params)}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _onboard_user(
|
|
164
|
+
access_token: str,
|
|
165
|
+
tier_id: str = "free-tier",
|
|
166
|
+
gcp_project_id: str = "",
|
|
167
|
+
) -> str:
|
|
168
|
+
"""Onboard user to get a managed project ID.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
access_token: OAuth access token
|
|
172
|
+
tier_id: Tier to onboard with ("free-tier" or "standard-tier")
|
|
173
|
+
gcp_project_id: Required for standard-tier - user's GCP project ID
|
|
174
|
+
"""
|
|
175
|
+
headers = {
|
|
176
|
+
"Authorization": f"Bearer {access_token}",
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
**ANTIGRAVITY_HEADERS,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
request_body: dict = {
|
|
182
|
+
"tierId": tier_id,
|
|
183
|
+
"metadata": {
|
|
184
|
+
"ideType": "IDE_UNSPECIFIED",
|
|
185
|
+
"platform": "PLATFORM_UNSPECIFIED",
|
|
186
|
+
"pluginType": "GEMINI",
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# For standard tier, add the user's GCP project ID
|
|
191
|
+
if tier_id == "standard-tier" and gcp_project_id:
|
|
192
|
+
request_body["cloudaicompanionProject"] = {"id": gcp_project_id}
|
|
193
|
+
|
|
194
|
+
for base_endpoint in ANTIGRAVITY_ENDPOINT_FALLBACKS:
|
|
195
|
+
for attempt in range(5): # Retry up to 5 times
|
|
196
|
+
try:
|
|
197
|
+
url = f"{base_endpoint}/v1internal:onboardUser"
|
|
198
|
+
response = requests.post(
|
|
199
|
+
url,
|
|
200
|
+
headers=headers,
|
|
201
|
+
json=request_body,
|
|
202
|
+
timeout=30,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if not response.ok:
|
|
206
|
+
logger.debug(
|
|
207
|
+
"onboardUser failed: %d %s",
|
|
208
|
+
response.status_code,
|
|
209
|
+
response.text[:200],
|
|
210
|
+
)
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
data = response.json()
|
|
214
|
+
|
|
215
|
+
# Check if onboarding is complete
|
|
216
|
+
if data.get("done"):
|
|
217
|
+
project_id = (
|
|
218
|
+
data.get("response", {})
|
|
219
|
+
.get("cloudaicompanionProject", {})
|
|
220
|
+
.get("id")
|
|
221
|
+
)
|
|
222
|
+
if project_id:
|
|
223
|
+
logger.debug("Onboarding complete, project_id: %s", project_id)
|
|
224
|
+
return project_id
|
|
225
|
+
|
|
226
|
+
# Wait and retry if not done
|
|
227
|
+
import time
|
|
228
|
+
|
|
229
|
+
time.sleep(3)
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.debug("onboardUser error: %s", e)
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
return ""
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@dataclass
|
|
239
|
+
class AntigravityStatus:
|
|
240
|
+
"""Status information from Antigravity API."""
|
|
241
|
+
|
|
242
|
+
project_id: str = ""
|
|
243
|
+
current_tier: str = ""
|
|
244
|
+
allowed_tiers: List[str] = field(default_factory=list)
|
|
245
|
+
is_onboarded: bool = False
|
|
246
|
+
error: Optional[str] = None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def fetch_antigravity_status(access_token: str) -> AntigravityStatus:
|
|
250
|
+
"""Fetch full status from Antigravity loadCodeAssist API."""
|
|
251
|
+
headers = {
|
|
252
|
+
"Authorization": f"Bearer {access_token}",
|
|
253
|
+
"Content-Type": "application/json",
|
|
254
|
+
"User-Agent": "google-api-nodejs-client/9.15.1",
|
|
255
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
256
|
+
"Client-Metadata": ANTIGRAVITY_HEADERS["Client-Metadata"],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
endpoints = list(
|
|
260
|
+
dict.fromkeys(ANTIGRAVITY_LOAD_ENDPOINTS + list(ANTIGRAVITY_ENDPOINT_FALLBACKS))
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
for base_endpoint in endpoints:
|
|
264
|
+
try:
|
|
265
|
+
url = f"{base_endpoint}/v1internal:loadCodeAssist"
|
|
266
|
+
response = requests.post(
|
|
267
|
+
url,
|
|
268
|
+
headers=headers,
|
|
269
|
+
json={
|
|
270
|
+
"metadata": {
|
|
271
|
+
"ideType": "IDE_UNSPECIFIED",
|
|
272
|
+
"platform": "PLATFORM_UNSPECIFIED",
|
|
273
|
+
"pluginType": "GEMINI",
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
timeout=30,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if not response.ok:
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
data = response.json()
|
|
283
|
+
|
|
284
|
+
# Extract project info
|
|
285
|
+
project_id = ""
|
|
286
|
+
project = data.get("cloudaicompanionProject")
|
|
287
|
+
if isinstance(project, str) and project:
|
|
288
|
+
project_id = project
|
|
289
|
+
elif isinstance(project, dict) and project.get("id"):
|
|
290
|
+
project_id = project["id"]
|
|
291
|
+
|
|
292
|
+
# Extract tier info
|
|
293
|
+
allowed_tiers_data = data.get("allowedTiers", [])
|
|
294
|
+
allowed_tier_ids = [
|
|
295
|
+
t.get("id", "") for t in allowed_tiers_data if t.get("id")
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
# Find current tier (the one marked as default or the one with project)
|
|
299
|
+
current_tier = ""
|
|
300
|
+
for tier in allowed_tiers_data:
|
|
301
|
+
if tier.get("isDefault"):
|
|
302
|
+
current_tier = tier.get("id", "")
|
|
303
|
+
break
|
|
304
|
+
# If project exists and tier doesn't require user-defined project, it's likely current
|
|
305
|
+
if project_id and not tier.get("userDefinedCloudaicompanionProject"):
|
|
306
|
+
current_tier = tier.get("id", "")
|
|
307
|
+
|
|
308
|
+
return AntigravityStatus(
|
|
309
|
+
project_id=project_id,
|
|
310
|
+
current_tier=current_tier,
|
|
311
|
+
allowed_tiers=allowed_tier_ids,
|
|
312
|
+
is_onboarded=bool(project_id),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
except Exception:
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
return AntigravityStatus(error="Could not reach Antigravity API")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _fetch_project_id(access_token: str) -> str:
|
|
322
|
+
"""Fetch project ID from Antigravity loadCodeAssist API."""
|
|
323
|
+
errors: List[str] = []
|
|
324
|
+
|
|
325
|
+
headers = {
|
|
326
|
+
"Authorization": f"Bearer {access_token}",
|
|
327
|
+
"Content-Type": "application/json",
|
|
328
|
+
"User-Agent": "google-api-nodejs-client/9.15.1",
|
|
329
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
330
|
+
"Client-Metadata": ANTIGRAVITY_HEADERS["Client-Metadata"],
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
# Try each endpoint in order (deduplicated)
|
|
334
|
+
endpoints = list(
|
|
335
|
+
dict.fromkeys(ANTIGRAVITY_LOAD_ENDPOINTS + list(ANTIGRAVITY_ENDPOINT_FALLBACKS))
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# First, try to get existing project from loadCodeAssist
|
|
339
|
+
allowed_tiers: List[dict] = []
|
|
340
|
+
|
|
341
|
+
for base_endpoint in endpoints:
|
|
342
|
+
try:
|
|
343
|
+
url = f"{base_endpoint}/v1internal:loadCodeAssist"
|
|
344
|
+
response = requests.post(
|
|
345
|
+
url,
|
|
346
|
+
headers=headers,
|
|
347
|
+
json={
|
|
348
|
+
"metadata": {
|
|
349
|
+
"ideType": "IDE_UNSPECIFIED",
|
|
350
|
+
"platform": "PLATFORM_UNSPECIFIED",
|
|
351
|
+
"pluginType": "GEMINI",
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
timeout=30,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
if not response.ok:
|
|
358
|
+
errors.append(
|
|
359
|
+
f"loadCodeAssist {response.status_code} at {base_endpoint}: "
|
|
360
|
+
f"{response.text[:200]}"
|
|
361
|
+
)
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
data = response.json()
|
|
365
|
+
|
|
366
|
+
# Try to extract project ID from response
|
|
367
|
+
project = data.get("cloudaicompanionProject")
|
|
368
|
+
|
|
369
|
+
if isinstance(project, str) and project:
|
|
370
|
+
return project
|
|
371
|
+
if isinstance(project, dict) and project.get("id"):
|
|
372
|
+
return project["id"]
|
|
373
|
+
|
|
374
|
+
# Store allowed tiers for potential onboarding
|
|
375
|
+
if data.get("allowedTiers"):
|
|
376
|
+
allowed_tiers = data.get("allowedTiers", [])
|
|
377
|
+
|
|
378
|
+
errors.append(f"loadCodeAssist missing project id at {base_endpoint}")
|
|
379
|
+
|
|
380
|
+
except Exception as e:
|
|
381
|
+
errors.append(f"loadCodeAssist error at {base_endpoint}: {e}")
|
|
382
|
+
|
|
383
|
+
# No project found - try to onboard with free tier if available
|
|
384
|
+
if allowed_tiers:
|
|
385
|
+
# Find the default tier or free tier
|
|
386
|
+
default_tier = None
|
|
387
|
+
for tier in allowed_tiers:
|
|
388
|
+
if tier.get("isDefault"):
|
|
389
|
+
default_tier = tier
|
|
390
|
+
break
|
|
391
|
+
if tier.get("id") == "free-tier":
|
|
392
|
+
default_tier = tier
|
|
393
|
+
|
|
394
|
+
if default_tier and not default_tier.get("userDefinedCloudaicompanionProject"):
|
|
395
|
+
tier_id = default_tier.get("id", "free-tier")
|
|
396
|
+
logger.debug(
|
|
397
|
+
"No project found, attempting onboarding with tier: %s", tier_id
|
|
398
|
+
)
|
|
399
|
+
project_id = _onboard_user(access_token, tier_id)
|
|
400
|
+
if project_id:
|
|
401
|
+
return project_id
|
|
402
|
+
|
|
403
|
+
if errors:
|
|
404
|
+
logger.debug(
|
|
405
|
+
"Could not resolve Antigravity project (non-fatal): %s", "; ".join(errors)
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
return ""
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def exchange_code_for_tokens(
|
|
412
|
+
code: str,
|
|
413
|
+
state: str,
|
|
414
|
+
redirect_uri: str,
|
|
415
|
+
) -> TokenExchangeResult:
|
|
416
|
+
"""Exchange an authorization code for Antigravity OAuth tokens."""
|
|
417
|
+
try:
|
|
418
|
+
# Decode and verify state
|
|
419
|
+
verifier, project_id = _decode_state(state)
|
|
420
|
+
|
|
421
|
+
# Exchange code for tokens
|
|
422
|
+
response = requests.post(
|
|
423
|
+
"https://oauth2.googleapis.com/token",
|
|
424
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
425
|
+
data={
|
|
426
|
+
"client_id": ANTIGRAVITY_CLIENT_ID,
|
|
427
|
+
"client_secret": ANTIGRAVITY_CLIENT_SECRET,
|
|
428
|
+
"code": code,
|
|
429
|
+
"grant_type": "authorization_code",
|
|
430
|
+
"redirect_uri": redirect_uri,
|
|
431
|
+
"code_verifier": verifier,
|
|
432
|
+
},
|
|
433
|
+
timeout=30,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
if not response.ok:
|
|
437
|
+
return TokenExchangeFailure(error=response.text)
|
|
438
|
+
|
|
439
|
+
token_data = response.json()
|
|
440
|
+
access_token = token_data.get("access_token", "")
|
|
441
|
+
refresh_token = token_data.get("refresh_token", "")
|
|
442
|
+
expires_in = token_data.get("expires_in", 3600)
|
|
443
|
+
|
|
444
|
+
if not refresh_token:
|
|
445
|
+
return TokenExchangeFailure(error="Missing refresh token in response")
|
|
446
|
+
|
|
447
|
+
# Fetch user email
|
|
448
|
+
email: Optional[str] = None
|
|
449
|
+
try:
|
|
450
|
+
user_response = requests.get(
|
|
451
|
+
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
|
452
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
|
453
|
+
timeout=10,
|
|
454
|
+
)
|
|
455
|
+
if user_response.ok:
|
|
456
|
+
email = user_response.json().get("email")
|
|
457
|
+
except Exception as e:
|
|
458
|
+
logger.warning("Failed to fetch user email: %s", e)
|
|
459
|
+
|
|
460
|
+
# Try to get project ID if not provided
|
|
461
|
+
effective_project_id = project_id
|
|
462
|
+
if not effective_project_id:
|
|
463
|
+
effective_project_id = _fetch_project_id(access_token)
|
|
464
|
+
|
|
465
|
+
# Format refresh token with project ID
|
|
466
|
+
stored_refresh = f"{refresh_token}|{effective_project_id or ''}"
|
|
467
|
+
|
|
468
|
+
return TokenExchangeSuccess(
|
|
469
|
+
refresh_token=stored_refresh,
|
|
470
|
+
access_token=access_token,
|
|
471
|
+
expires_at=time.time() + expires_in,
|
|
472
|
+
email=email,
|
|
473
|
+
project_id=effective_project_id or "",
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
except Exception as e:
|
|
477
|
+
logger.exception("Token exchange failed")
|
|
478
|
+
return TokenExchangeFailure(error=str(e))
|