Skip to content

Commit bed2b16

Browse files
cristipufuclaude
andcommitted
fix: reload runtime factory after login to pick up new credentials
The UiPath runtime caches credentials at init time via UiPathApiConfig. When the server starts unauthenticated and the user logs in later, os.environ gets updated but the existing factory still holds stale (empty) credentials, causing 401s on subsequent API calls. - Add _on_authenticated callback to AuthState, wired in create_app to trigger server.reload_factory() after successful login - Use _update_env_file (mirrors uipath._utils._auth.update_env_file) to write .env the same way as `uipath auth` - Use load_dotenv(dotenv_path=cwd/.env, override=True) matching the CLI root behavior - Bump version to 0.0.69 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 92ed32c commit bed2b16

4 files changed

Lines changed: 109 additions & 43 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-dev"
3-
version = "0.0.68"
3+
version = "0.0.69"
44
description = "UiPath Developer Console"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/dev/server/app.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import logging
67
import os
78
from pathlib import Path
@@ -160,12 +161,37 @@ async def _config():
160161
from uipath.dev.server.ws.handler import router as ws_router
161162

162163
if auth_enabled:
163-
from uipath.dev.server.auth import restore_session
164+
from uipath.dev.server.auth import get_auth_state, restore_session
164165
from uipath.dev.server.routes.auth import router as auth_router
165166

166167
app.include_router(auth_router, prefix="/api")
167168
restore_session()
168169

170+
# Reload the runtime factory when authentication completes so the
171+
# newly-written credentials are picked up by subsequent runs.
172+
def _on_authenticated() -> None:
173+
async def _safe_reload() -> None:
174+
# Wait for active runs to finish before reloading
175+
while any(
176+
r.status in ("pending", "running")
177+
for r in server.run_service.runs.values()
178+
):
179+
await asyncio.sleep(1)
180+
await server.reload_factory()
181+
182+
def _on_reload_done(t: asyncio.Task[None]) -> None:
183+
try:
184+
t.result()
185+
except asyncio.CancelledError:
186+
pass
187+
except Exception:
188+
logger.exception("Factory reload after login failed")
189+
190+
task = asyncio.create_task(_safe_reload())
191+
task.add_done_callback(_on_reload_done)
192+
193+
get_auth_state()._on_authenticated = _on_authenticated
194+
169195
app.include_router(entrypoints_router, prefix="/api")
170196
app.include_router(runs_router, prefix="/api")
171197
app.include_router(graph_router, prefix="/api")

src/uipath/dev/server/auth.py

Lines changed: 80 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import socketserver
1919
import threading
2020
import time
21+
from collections.abc import Callable
2122
from dataclasses import dataclass, field
2223
from pathlib import Path
2324
from typing import Any
@@ -109,6 +110,8 @@ class AuthState:
109110
_last_tenant: str | None = None
110111
_last_org: dict[str, str] = field(default_factory=dict)
111112
_last_environment: str | None = None
113+
# Callback invoked after successful authentication
114+
_on_authenticated: Callable[[], None] | None = None
112115
# internal
113116
_code_verifier: str | None = None
114117
_state: str | None = None
@@ -128,9 +131,30 @@ def get_auth_state() -> AuthState:
128131

129132

130133
def reset_auth_state() -> None:
131-
"""Reset the auth state to its initial (unauthenticated) values."""
132-
global _auth
133-
_auth = AuthState()
134+
"""Reset the auth state to its initial (unauthenticated) values.
135+
136+
Preserves registered callbacks (e.g. ``_on_authenticated``).
137+
"""
138+
_auth.status = "unauthenticated"
139+
_auth.environment = "cloud"
140+
_auth.token_data = {}
141+
_auth.tenants = []
142+
_auth.organization = {}
143+
_auth.uipath_url = None
144+
_auth._last_tenant = None
145+
_auth._last_org = {}
146+
_auth._last_environment = None
147+
_auth._code_verifier = None
148+
_auth._state = None
149+
_auth._port = None
150+
_auth._callback_server = None
151+
if _auth._wait_task and not _auth._wait_task.done():
152+
_auth._wait_task.cancel()
153+
_auth._wait_task = None
154+
if _auth._token_event:
155+
_auth._token_event.set()
156+
_auth._token_event = None
157+
_auth._loop = None
134158

135159

136160
# ---------------------------------------------------------------------------
@@ -990,6 +1014,40 @@ def select_tenant(tenant_name: str) -> dict[str, Any]:
9901014
return {"status": "authenticated", "uipath_url": auth.uipath_url}
9911015

9921016

1017+
def _update_env_file(env_contents: dict[str, str]) -> None:
1018+
"""Merge *env_contents* into the CWD ``.env`` file.
1019+
1020+
New keys take priority; existing keys not in *env_contents* are preserved.
1021+
Comments and blank lines are kept as-is.
1022+
"""
1023+
env_path = Path.cwd() / ".env"
1024+
lines: list[str] = []
1025+
seen_keys: set[str] = set()
1026+
1027+
if env_path.exists():
1028+
with open(env_path) as f:
1029+
for raw_line in f:
1030+
stripped = raw_line.strip()
1031+
if stripped.startswith("#") or "=" not in stripped:
1032+
# Preserve comments and blank lines
1033+
lines.append(raw_line)
1034+
continue
1035+
key = stripped.split("=", 1)[0]
1036+
if key in env_contents:
1037+
lines.append(f"{key}={env_contents[key]}\n")
1038+
else:
1039+
lines.append(raw_line)
1040+
seen_keys.add(key)
1041+
1042+
# Append new keys that weren't already in the file
1043+
for key, value in env_contents.items():
1044+
if key not in seen_keys:
1045+
lines.append(f"{key}={value}\n")
1046+
1047+
with open(env_path, "w") as f:
1048+
f.writelines(lines)
1049+
1050+
9931051
def _finalize_tenant(auth: AuthState, tenant_name: str) -> None:
9941052
"""Write .env and os.environ with the resolved credentials."""
9951053
org_name = auth.organization.get("name", "")
@@ -1010,45 +1068,27 @@ def _finalize_tenant(auth: AuthState, tenant_name: str) -> None:
10101068
auth._last_org = dict(auth.organization)
10111069
auth._last_environment = auth.environment
10121070

1013-
# Update os.environ
1014-
os.environ["UIPATH_ACCESS_TOKEN"] = access_token
1015-
os.environ["UIPATH_URL"] = uipath_url
1016-
os.environ["UIPATH_TENANT_ID"] = tenant_id
1017-
os.environ["UIPATH_ORGANIZATION_ID"] = org_id
1018-
1019-
# Write/update .env file (preserving comments, blank lines, and ordering)
1020-
env_path = Path.cwd() / ".env"
1021-
lines: list[str] = []
1022-
updated_keys: set[str] = set()
1023-
new_values = {
1024-
"UIPATH_ACCESS_TOKEN": access_token,
1025-
"UIPATH_URL": uipath_url,
1026-
"UIPATH_TENANT_ID": tenant_id,
1027-
"UIPATH_ORGANIZATION_ID": org_id,
1028-
}
1029-
1030-
if env_path.exists():
1031-
with open(env_path) as f:
1032-
for raw_line in f:
1033-
stripped = raw_line.strip()
1034-
if "=" in stripped and not stripped.startswith("#"):
1035-
key = stripped.split("=", 1)[0]
1036-
if key in new_values:
1037-
lines.append(f"{key}={new_values[key]}\n")
1038-
updated_keys.add(key)
1039-
continue
1040-
lines.append(raw_line)
1041-
1042-
# Append any keys that weren't already in the file
1043-
for key, value in new_values.items():
1044-
if key not in updated_keys:
1045-
lines.append(f"{key}={value}\n")
1071+
# Write .env using the same approach as `uipath auth`
1072+
_update_env_file(
1073+
{
1074+
"UIPATH_ACCESS_TOKEN": access_token,
1075+
"UIPATH_URL": uipath_url,
1076+
"UIPATH_TENANT_ID": tenant_id,
1077+
"UIPATH_ORGANIZATION_ID": org_id,
1078+
}
1079+
)
10461080

1047-
with open(env_path, "w") as f:
1048-
f.writelines(lines)
1081+
# Reload .env into os.environ (same as CLI root: cwd + override)
1082+
load_dotenv(
1083+
dotenv_path=os.path.join(os.getcwd(), ".env"),
1084+
override=True,
1085+
)
10491086

1050-
# Reload all .env variables into os.environ
1051-
load_dotenv(override=True)
1087+
if auth._on_authenticated:
1088+
try:
1089+
auth._on_authenticated()
1090+
except Exception:
1091+
logger.exception("Error in post-authentication callback")
10521092

10531093

10541094
def logout() -> None:

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)