pdd-cli 0.0.90__py3-none-any.whl → 0.0.118__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.
- pdd/__init__.py +38 -6
- pdd/agentic_bug.py +323 -0
- pdd/agentic_bug_orchestrator.py +497 -0
- pdd/agentic_change.py +231 -0
- pdd/agentic_change_orchestrator.py +526 -0
- pdd/agentic_common.py +521 -786
- pdd/agentic_e2e_fix.py +319 -0
- pdd/agentic_e2e_fix_orchestrator.py +426 -0
- pdd/agentic_fix.py +118 -3
- pdd/agentic_update.py +25 -8
- pdd/architecture_sync.py +565 -0
- pdd/auth_service.py +210 -0
- pdd/auto_deps_main.py +63 -53
- pdd/auto_include.py +185 -3
- pdd/auto_update.py +125 -47
- pdd/bug_main.py +195 -23
- pdd/cmd_test_main.py +345 -197
- pdd/code_generator.py +4 -2
- pdd/code_generator_main.py +118 -32
- pdd/commands/__init__.py +6 -0
- pdd/commands/analysis.py +87 -29
- pdd/commands/auth.py +309 -0
- pdd/commands/connect.py +290 -0
- pdd/commands/fix.py +136 -113
- pdd/commands/maintenance.py +3 -2
- pdd/commands/misc.py +8 -0
- pdd/commands/modify.py +190 -164
- pdd/commands/sessions.py +284 -0
- pdd/construct_paths.py +334 -32
- pdd/context_generator_main.py +167 -170
- pdd/continue_generation.py +6 -3
- pdd/core/__init__.py +33 -0
- pdd/core/cli.py +27 -3
- pdd/core/cloud.py +237 -0
- pdd/core/errors.py +4 -0
- pdd/core/remote_session.py +61 -0
- pdd/crash_main.py +219 -23
- pdd/data/llm_model.csv +4 -4
- pdd/docs/prompting_guide.md +864 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/benchmark_analysis.py +495 -0
- pdd/docs/whitepaper_with_benchmarks/data_and_functions/creation_compare.py +528 -0
- pdd/fix_code_loop.py +208 -34
- pdd/fix_code_module_errors.py +6 -2
- pdd/fix_error_loop.py +291 -38
- pdd/fix_main.py +204 -4
- pdd/fix_verification_errors_loop.py +235 -26
- pdd/fix_verification_main.py +269 -83
- pdd/frontend/dist/assets/index-B5DZHykP.css +1 -0
- pdd/frontend/dist/assets/index-DQ3wkeQ2.js +449 -0
- pdd/frontend/dist/index.html +376 -0
- pdd/frontend/dist/logo.svg +33 -0
- pdd/generate_output_paths.py +46 -5
- pdd/generate_test.py +212 -151
- pdd/get_comment.py +19 -44
- pdd/get_extension.py +8 -9
- pdd/get_jwt_token.py +309 -20
- pdd/get_language.py +8 -7
- pdd/get_run_command.py +7 -5
- pdd/insert_includes.py +2 -1
- pdd/llm_invoke.py +459 -95
- pdd/load_prompt_template.py +15 -34
- pdd/path_resolution.py +140 -0
- pdd/postprocess.py +4 -1
- pdd/preprocess.py +68 -12
- pdd/preprocess_main.py +33 -1
- pdd/prompts/agentic_bug_step10_pr_LLM.prompt +182 -0
- pdd/prompts/agentic_bug_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_bug_step2_docs_LLM.prompt +129 -0
- pdd/prompts/agentic_bug_step3_triage_LLM.prompt +95 -0
- pdd/prompts/agentic_bug_step4_reproduce_LLM.prompt +97 -0
- pdd/prompts/agentic_bug_step5_root_cause_LLM.prompt +123 -0
- pdd/prompts/agentic_bug_step6_test_plan_LLM.prompt +107 -0
- pdd/prompts/agentic_bug_step7_generate_LLM.prompt +172 -0
- pdd/prompts/agentic_bug_step8_verify_LLM.prompt +119 -0
- pdd/prompts/agentic_bug_step9_e2e_test_LLM.prompt +289 -0
- pdd/prompts/agentic_change_step10_identify_issues_LLM.prompt +1006 -0
- pdd/prompts/agentic_change_step11_fix_issues_LLM.prompt +984 -0
- pdd/prompts/agentic_change_step12_create_pr_LLM.prompt +131 -0
- pdd/prompts/agentic_change_step1_duplicate_LLM.prompt +73 -0
- pdd/prompts/agentic_change_step2_docs_LLM.prompt +101 -0
- pdd/prompts/agentic_change_step3_research_LLM.prompt +126 -0
- pdd/prompts/agentic_change_step4_clarify_LLM.prompt +164 -0
- pdd/prompts/agentic_change_step5_docs_change_LLM.prompt +981 -0
- pdd/prompts/agentic_change_step6_devunits_LLM.prompt +1005 -0
- pdd/prompts/agentic_change_step7_architecture_LLM.prompt +1044 -0
- pdd/prompts/agentic_change_step8_analyze_LLM.prompt +1027 -0
- pdd/prompts/agentic_change_step9_implement_LLM.prompt +1077 -0
- pdd/prompts/agentic_e2e_fix_step1_unit_tests_LLM.prompt +90 -0
- pdd/prompts/agentic_e2e_fix_step2_e2e_tests_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step3_root_cause_LLM.prompt +89 -0
- pdd/prompts/agentic_e2e_fix_step4_fix_e2e_tests_LLM.prompt +96 -0
- pdd/prompts/agentic_e2e_fix_step5_identify_devunits_LLM.prompt +91 -0
- pdd/prompts/agentic_e2e_fix_step6_create_unit_tests_LLM.prompt +106 -0
- pdd/prompts/agentic_e2e_fix_step7_verify_tests_LLM.prompt +116 -0
- pdd/prompts/agentic_e2e_fix_step8_run_pdd_fix_LLM.prompt +120 -0
- pdd/prompts/agentic_e2e_fix_step9_verify_all_LLM.prompt +146 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +2 -2
- pdd/prompts/agentic_update_LLM.prompt +192 -338
- pdd/prompts/auto_include_LLM.prompt +22 -0
- pdd/prompts/change_LLM.prompt +3093 -1
- pdd/prompts/detect_change_LLM.prompt +571 -14
- pdd/prompts/fix_code_module_errors_LLM.prompt +8 -0
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +1 -0
- pdd/prompts/generate_test_LLM.prompt +20 -1
- pdd/prompts/generate_test_from_example_LLM.prompt +115 -0
- pdd/prompts/insert_includes_LLM.prompt +262 -252
- pdd/prompts/prompt_code_diff_LLM.prompt +119 -0
- pdd/prompts/prompt_diff_LLM.prompt +82 -0
- pdd/remote_session.py +876 -0
- pdd/server/__init__.py +52 -0
- pdd/server/app.py +335 -0
- pdd/server/click_executor.py +587 -0
- pdd/server/executor.py +338 -0
- pdd/server/jobs.py +661 -0
- pdd/server/models.py +241 -0
- pdd/server/routes/__init__.py +31 -0
- pdd/server/routes/architecture.py +451 -0
- pdd/server/routes/auth.py +364 -0
- pdd/server/routes/commands.py +929 -0
- pdd/server/routes/config.py +42 -0
- pdd/server/routes/files.py +603 -0
- pdd/server/routes/prompts.py +1322 -0
- pdd/server/routes/websocket.py +473 -0
- pdd/server/security.py +243 -0
- pdd/server/terminal_spawner.py +209 -0
- pdd/server/token_counter.py +222 -0
- pdd/summarize_directory.py +236 -237
- pdd/sync_animation.py +8 -4
- pdd/sync_determine_operation.py +329 -47
- pdd/sync_main.py +272 -28
- pdd/sync_orchestration.py +136 -75
- pdd/template_expander.py +161 -0
- pdd/templates/architecture/architecture_json.prompt +41 -46
- pdd/trace.py +1 -1
- pdd/track_cost.py +0 -13
- pdd/unfinished_prompt.py +2 -1
- pdd/update_main.py +23 -5
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/METADATA +15 -10
- pdd_cli-0.0.118.dist-info/RECORD +227 -0
- pdd_cli-0.0.90.dist-info/RECORD +0 -153
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/licenses/LICENSE +0 -0
- {pdd_cli-0.0.90.dist-info → pdd_cli-0.0.118.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""Authentication routes for PDD Cloud.
|
|
2
|
+
|
|
3
|
+
Provides endpoints to check authentication status, force re-authentication,
|
|
4
|
+
and trigger GitHub Device Flow login directly from the web UI.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
import webbrowser
|
|
12
|
+
from typing import Dict, Optional
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter, BackgroundTasks
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
from pdd.auth_service import (
|
|
18
|
+
get_jwt_cache_info as _get_jwt_cache_info,
|
|
19
|
+
has_refresh_token as _has_refresh_token,
|
|
20
|
+
clear_jwt_cache as _clear_jwt_cache,
|
|
21
|
+
clear_refresh_token as _clear_refresh_token,
|
|
22
|
+
get_cached_jwt as _get_cached_jwt,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
|
|
26
|
+
|
|
27
|
+
# Environment variable names (same as cloud.py)
|
|
28
|
+
FIREBASE_API_KEY_ENV = "NEXT_PUBLIC_FIREBASE_API_KEY"
|
|
29
|
+
GITHUB_CLIENT_ID_ENV = "GITHUB_CLIENT_ID"
|
|
30
|
+
|
|
31
|
+
# Active login sessions (poll_id -> session state)
|
|
32
|
+
_active_sessions: Dict[str, dict] = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AuthStatus(BaseModel):
|
|
36
|
+
"""Response model for authentication status."""
|
|
37
|
+
|
|
38
|
+
authenticated: bool
|
|
39
|
+
cached: bool
|
|
40
|
+
expires_at: Optional[float] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LogoutResult(BaseModel):
|
|
44
|
+
"""Response model for logout operation."""
|
|
45
|
+
|
|
46
|
+
success: bool
|
|
47
|
+
message: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LoginRequest(BaseModel):
|
|
51
|
+
"""Request model for starting login flow."""
|
|
52
|
+
|
|
53
|
+
no_browser: bool = False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LoginResponse(BaseModel):
|
|
57
|
+
"""Response model for starting login flow."""
|
|
58
|
+
|
|
59
|
+
success: bool
|
|
60
|
+
user_code: Optional[str] = None
|
|
61
|
+
verification_uri: Optional[str] = None
|
|
62
|
+
expires_in: Optional[int] = None
|
|
63
|
+
poll_id: Optional[str] = None
|
|
64
|
+
error: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class LoginPollResponse(BaseModel):
|
|
68
|
+
"""Response model for polling login status."""
|
|
69
|
+
|
|
70
|
+
status: str # "pending", "completed", "expired", "error"
|
|
71
|
+
message: Optional[str] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.get("/status", response_model=AuthStatus)
|
|
75
|
+
async def get_auth_status() -> AuthStatus:
|
|
76
|
+
"""
|
|
77
|
+
Check current authentication status.
|
|
78
|
+
|
|
79
|
+
Returns whether the user is authenticated (has valid cached JWT or refresh token).
|
|
80
|
+
"""
|
|
81
|
+
# First check JWT cache
|
|
82
|
+
cache_valid, expires_at = _get_jwt_cache_info()
|
|
83
|
+
if cache_valid:
|
|
84
|
+
return AuthStatus(authenticated=True, cached=True, expires_at=expires_at)
|
|
85
|
+
|
|
86
|
+
# Check for refresh token in keyring
|
|
87
|
+
has_refresh = _has_refresh_token()
|
|
88
|
+
if has_refresh:
|
|
89
|
+
return AuthStatus(authenticated=True, cached=False, expires_at=None)
|
|
90
|
+
|
|
91
|
+
return AuthStatus(authenticated=False, cached=False, expires_at=None)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class JWTTokenResponse(BaseModel):
|
|
95
|
+
"""Response model for JWT token."""
|
|
96
|
+
|
|
97
|
+
jwt: Optional[str] = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@router.get("/jwt-token", response_model=JWTTokenResponse)
|
|
101
|
+
async def get_jwt_token() -> JWTTokenResponse:
|
|
102
|
+
"""
|
|
103
|
+
Get the current JWT token from cache.
|
|
104
|
+
|
|
105
|
+
Returns the cached JWT token if valid, otherwise returns null.
|
|
106
|
+
Used by the frontend to authenticate with cloud services.
|
|
107
|
+
"""
|
|
108
|
+
token = _get_cached_jwt()
|
|
109
|
+
return JWTTokenResponse(jwt=token)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.post("/logout", response_model=LogoutResult)
|
|
113
|
+
async def logout() -> LogoutResult:
|
|
114
|
+
"""
|
|
115
|
+
Clear all authentication tokens to force fresh GitHub login.
|
|
116
|
+
|
|
117
|
+
Clears both the JWT cache file and the refresh token from keyring.
|
|
118
|
+
After calling this, the next pdd command will trigger the GitHub Device Flow.
|
|
119
|
+
"""
|
|
120
|
+
errors = []
|
|
121
|
+
|
|
122
|
+
# Clear JWT cache
|
|
123
|
+
jwt_success, jwt_error = _clear_jwt_cache()
|
|
124
|
+
if not jwt_success and jwt_error:
|
|
125
|
+
errors.append(jwt_error)
|
|
126
|
+
|
|
127
|
+
# Clear refresh token from keyring
|
|
128
|
+
refresh_success, refresh_error = _clear_refresh_token()
|
|
129
|
+
if not refresh_success and refresh_error:
|
|
130
|
+
errors.append(refresh_error)
|
|
131
|
+
|
|
132
|
+
if errors:
|
|
133
|
+
return LogoutResult(success=False, message="; ".join(errors))
|
|
134
|
+
|
|
135
|
+
return LogoutResult(
|
|
136
|
+
success=True,
|
|
137
|
+
message="Tokens cleared successfully.",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def _poll_for_auth(poll_id: str, device_code: str, interval: int, expires_in: int) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Background task that polls GitHub for authentication completion.
|
|
144
|
+
Updates the session state when auth completes or expires.
|
|
145
|
+
"""
|
|
146
|
+
from pdd.get_jwt_token import (
|
|
147
|
+
DeviceFlow,
|
|
148
|
+
FirebaseAuthenticator,
|
|
149
|
+
AuthError,
|
|
150
|
+
NetworkError,
|
|
151
|
+
TokenError,
|
|
152
|
+
UserCancelledError,
|
|
153
|
+
_cache_jwt,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
github_client_id = os.environ.get(GITHUB_CLIENT_ID_ENV)
|
|
157
|
+
firebase_api_key = os.environ.get(FIREBASE_API_KEY_ENV)
|
|
158
|
+
|
|
159
|
+
if not github_client_id or not firebase_api_key:
|
|
160
|
+
_active_sessions[poll_id]["status"] = "error"
|
|
161
|
+
_active_sessions[poll_id]["message"] = "Missing API credentials"
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
device_flow = DeviceFlow(github_client_id)
|
|
165
|
+
firebase_auth = FirebaseAuthenticator(firebase_api_key, "pdd")
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
# Poll for GitHub token
|
|
169
|
+
github_token = await device_flow.poll_for_token(device_code, interval, expires_in)
|
|
170
|
+
|
|
171
|
+
# Exchange for Firebase token
|
|
172
|
+
id_token, refresh_token = await firebase_auth.exchange_github_token_for_firebase_token(
|
|
173
|
+
github_token
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Store tokens
|
|
177
|
+
firebase_auth._store_refresh_token(refresh_token)
|
|
178
|
+
_cache_jwt(id_token)
|
|
179
|
+
|
|
180
|
+
_active_sessions[poll_id]["status"] = "completed"
|
|
181
|
+
_active_sessions[poll_id]["message"] = "Authentication successful!"
|
|
182
|
+
|
|
183
|
+
except UserCancelledError:
|
|
184
|
+
_active_sessions[poll_id]["status"] = "error"
|
|
185
|
+
_active_sessions[poll_id]["message"] = "User denied access on GitHub"
|
|
186
|
+
except AuthError as e:
|
|
187
|
+
if "expired" in str(e).lower() or "timed out" in str(e).lower():
|
|
188
|
+
_active_sessions[poll_id]["status"] = "expired"
|
|
189
|
+
_active_sessions[poll_id]["message"] = "Authentication timed out. Please try again."
|
|
190
|
+
else:
|
|
191
|
+
_active_sessions[poll_id]["status"] = "error"
|
|
192
|
+
_active_sessions[poll_id]["message"] = str(e)
|
|
193
|
+
except (NetworkError, TokenError) as e:
|
|
194
|
+
_active_sessions[poll_id]["status"] = "error"
|
|
195
|
+
_active_sessions[poll_id]["message"] = str(e)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
_active_sessions[poll_id]["status"] = "error"
|
|
198
|
+
_active_sessions[poll_id]["message"] = f"Unexpected error: {e}"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@router.post("/login", response_model=LoginResponse)
|
|
202
|
+
async def start_login(
|
|
203
|
+
background_tasks: BackgroundTasks,
|
|
204
|
+
request: LoginRequest = LoginRequest()
|
|
205
|
+
) -> LoginResponse:
|
|
206
|
+
"""
|
|
207
|
+
Start GitHub Device Flow authentication.
|
|
208
|
+
|
|
209
|
+
Clears existing tokens and initiates a new GitHub Device Flow.
|
|
210
|
+
Returns the user code and verification URL for the user to complete authentication.
|
|
211
|
+
Opens the browser automatically unless no_browser is True.
|
|
212
|
+
"""
|
|
213
|
+
# Check for required environment variables
|
|
214
|
+
github_client_id = os.environ.get(GITHUB_CLIENT_ID_ENV)
|
|
215
|
+
firebase_api_key = os.environ.get(FIREBASE_API_KEY_ENV)
|
|
216
|
+
|
|
217
|
+
if not github_client_id:
|
|
218
|
+
return LoginResponse(
|
|
219
|
+
success=False,
|
|
220
|
+
error=f"Environment variable {GITHUB_CLIENT_ID_ENV} not set. Cloud authentication not available.",
|
|
221
|
+
)
|
|
222
|
+
if not firebase_api_key:
|
|
223
|
+
return LoginResponse(
|
|
224
|
+
success=False,
|
|
225
|
+
error=f"Environment variable {FIREBASE_API_KEY_ENV} not set. Cloud authentication not available.",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Clear existing tokens first
|
|
229
|
+
_clear_jwt_cache()
|
|
230
|
+
_clear_refresh_token()
|
|
231
|
+
|
|
232
|
+
# Import DeviceFlow and exceptions
|
|
233
|
+
from pdd.get_jwt_token import DeviceFlow, AuthError, NetworkError
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
device_flow = DeviceFlow(github_client_id)
|
|
237
|
+
device_code_response = await device_flow.request_device_code()
|
|
238
|
+
|
|
239
|
+
# Generate poll ID and store session
|
|
240
|
+
poll_id = str(uuid.uuid4())
|
|
241
|
+
_active_sessions[poll_id] = {
|
|
242
|
+
"status": "pending",
|
|
243
|
+
"message": "Waiting for user to authenticate on GitHub...",
|
|
244
|
+
"created_at": time.time(),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# Open browser for user (unless disabled)
|
|
248
|
+
verification_uri = device_code_response["verification_uri"]
|
|
249
|
+
|
|
250
|
+
if not request.no_browser:
|
|
251
|
+
try:
|
|
252
|
+
webbrowser.open(verification_uri)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
# Log error but don't fail - user can still open manually
|
|
255
|
+
import logging
|
|
256
|
+
logging.warning(f"Failed to open browser: {e}")
|
|
257
|
+
|
|
258
|
+
# Start background polling task
|
|
259
|
+
background_tasks.add_task(
|
|
260
|
+
_poll_for_auth,
|
|
261
|
+
poll_id,
|
|
262
|
+
device_code_response["device_code"],
|
|
263
|
+
device_code_response["interval"],
|
|
264
|
+
device_code_response["expires_in"],
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return LoginResponse(
|
|
268
|
+
success=True,
|
|
269
|
+
user_code=device_code_response["user_code"],
|
|
270
|
+
verification_uri=verification_uri,
|
|
271
|
+
expires_in=device_code_response["expires_in"],
|
|
272
|
+
poll_id=poll_id,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
except (AuthError, NetworkError) as e:
|
|
276
|
+
return LoginResponse(success=False, error=str(e))
|
|
277
|
+
except Exception as e:
|
|
278
|
+
return LoginResponse(success=False, error=f"Failed to start authentication: {e}")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@router.get("/login/poll/{poll_id}", response_model=LoginPollResponse)
|
|
282
|
+
async def poll_login_status(poll_id: str) -> LoginPollResponse:
|
|
283
|
+
"""
|
|
284
|
+
Poll for login completion status.
|
|
285
|
+
|
|
286
|
+
Returns the current status of the authentication flow.
|
|
287
|
+
"""
|
|
288
|
+
if poll_id not in _active_sessions:
|
|
289
|
+
return LoginPollResponse(status="error", message="Invalid or expired session")
|
|
290
|
+
|
|
291
|
+
session = _active_sessions[poll_id]
|
|
292
|
+
|
|
293
|
+
# Clean up completed/expired sessions after returning status
|
|
294
|
+
if session["status"] in ("completed", "expired", "error"):
|
|
295
|
+
# Keep for a short time so client can get final status
|
|
296
|
+
if time.time() - session.get("created_at", 0) > 60:
|
|
297
|
+
del _active_sessions[poll_id]
|
|
298
|
+
|
|
299
|
+
return LoginPollResponse(status=session["status"], message=session.get("message"))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class CloudConnectionTestResponse(BaseModel):
|
|
303
|
+
"""Response model for cloud connection test."""
|
|
304
|
+
|
|
305
|
+
connected: bool
|
|
306
|
+
session_count: Optional[int] = None
|
|
307
|
+
error: Optional[str] = None
|
|
308
|
+
cloud_url: str
|
|
309
|
+
environment: str
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@router.get("/test-cloud-connection", response_model=CloudConnectionTestResponse)
|
|
313
|
+
async def test_cloud_connection() -> CloudConnectionTestResponse:
|
|
314
|
+
"""
|
|
315
|
+
Test JWT token validity by calling cloud's /listSessions endpoint.
|
|
316
|
+
|
|
317
|
+
This helps diagnose connectivity issues and validates that:
|
|
318
|
+
1. JWT token is present and valid
|
|
319
|
+
2. Cloud URL is accessible
|
|
320
|
+
3. Token has correct permissions
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
CloudConnectionTestResponse with connection status and session count
|
|
324
|
+
"""
|
|
325
|
+
from pdd.core.cloud import CloudConfig
|
|
326
|
+
from pdd.remote_session import RemoteSessionManager
|
|
327
|
+
import os
|
|
328
|
+
|
|
329
|
+
cloud_url = CloudConfig.get_base_url()
|
|
330
|
+
environment = os.environ.get("PDD_ENV", "production")
|
|
331
|
+
|
|
332
|
+
# Get JWT token
|
|
333
|
+
jwt_token = _get_cached_jwt()
|
|
334
|
+
if not jwt_token:
|
|
335
|
+
return CloudConnectionTestResponse(
|
|
336
|
+
connected=False,
|
|
337
|
+
error="No JWT token found. Please authenticate with 'pdd auth login'.",
|
|
338
|
+
cloud_url=cloud_url,
|
|
339
|
+
environment=environment
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Try to list sessions
|
|
343
|
+
try:
|
|
344
|
+
sessions = await RemoteSessionManager.list_sessions(jwt_token)
|
|
345
|
+
return CloudConnectionTestResponse(
|
|
346
|
+
connected=True,
|
|
347
|
+
session_count=len(sessions),
|
|
348
|
+
cloud_url=cloud_url,
|
|
349
|
+
environment=environment
|
|
350
|
+
)
|
|
351
|
+
except Exception as e:
|
|
352
|
+
error_msg = str(e)
|
|
353
|
+
# Parse common error types
|
|
354
|
+
if "401" in error_msg or "403" in error_msg or "Unauthorized" in error_msg:
|
|
355
|
+
error_msg = f"Authentication failed: {error_msg}. Token may be expired or invalid."
|
|
356
|
+
elif "timeout" in error_msg.lower() or "connection" in error_msg.lower():
|
|
357
|
+
error_msg = f"Network error: {error_msg}. Cloud may be unreachable."
|
|
358
|
+
|
|
359
|
+
return CloudConnectionTestResponse(
|
|
360
|
+
connected=False,
|
|
361
|
+
error=error_msg,
|
|
362
|
+
cloud_url=cloud_url,
|
|
363
|
+
environment=environment
|
|
364
|
+
)
|