Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-dev"
version = "0.0.68"
version = "0.0.69"
description = "UiPath Developer Console"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
10 changes: 9 additions & 1 deletion src/uipath/dev/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import asyncio
import logging
import os
from pathlib import Path
Expand Down Expand Up @@ -160,12 +161,19 @@ async def _config():
from uipath.dev.server.ws.handler import router as ws_router

if auth_enabled:
from uipath.dev.server.auth import restore_session
from uipath.dev.server.auth import get_auth_state, restore_session
from uipath.dev.server.routes.auth import router as auth_router

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

# Reload the runtime factory when authentication completes so the
# newly-written credentials are picked up by subsequent runs.
def _on_authenticated() -> None:
asyncio.ensure_future(server.reload_factory())
Comment on lines +170 to +173
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

server.reload_factory() can dispose/recreate the runtime while runs are active, but this callback path does not apply the same “no active runs” guard used by the /reload endpoint. Consider reusing the same active-run check here (or deferring reload until idle) to avoid disrupting in-flight runs if a user authenticates mid-execution.

Suggested change
# Reload the runtime factory when authentication completes so the
# newly-written credentials are picked up by subsequent runs.
def _on_authenticated() -> None:
asyncio.ensure_future(server.reload_factory())
# When authentication completes, avoid reloading the runtime factory
# directly from this callback to prevent disrupting any in-flight runs.
# Use the existing guarded reload mechanisms (e.g. the /reload endpoint)
# to apply newly-written credentials safely.
def _on_authenticated() -> None:
logger.info(
"Authentication completed; runtime reload should be performed via the "
"guarded /reload endpoint to apply new credentials safely."
)

Copilot uses AI. Check for mistakes.

Comment on lines +173 to +174
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _on_authenticated hook schedules server.reload_factory() via asyncio.ensure_future(...) but does not retain the task or handle errors, so failures can surface as “Task exception was never retrieved” and be easy to miss. Consider using asyncio.create_task(...) and adding a done-callback (or other mechanism) to log/handle exceptions from reload_factory.

Suggested change
asyncio.ensure_future(server.reload_factory())
task = asyncio.create_task(server.reload_factory())
def _log_reload_result(t: asyncio.Task) -> None:
try:
t.result()
except Exception:
logger.exception(
"Error while reloading runtime factory after authentication"
)
task.add_done_callback(_log_reload_result)

Copilot uses AI. Check for mistakes.
get_auth_state()._on_authenticated = _on_authenticated

app.include_router(entrypoints_router, prefix="/api")
app.include_router(runs_router, prefix="/api")
app.include_router(graph_router, prefix="/api")
Expand Down
74 changes: 37 additions & 37 deletions src/uipath/dev/server/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ class AuthState:
_last_tenant: str | None = None
_last_org: dict[str, str] = field(default_factory=dict)
_last_environment: str | None = None
# Callback invoked after successful authentication
_on_authenticated: Any | None = None
Comment on lines +112 to +113
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_on_authenticated is typed as Any | None, which loses type-safety and makes it unclear what call signature is expected. Consider typing it as a Callable[[], None] | None (or Callable[[], Awaitable[None]] | None if async callbacks are allowed) and/or providing a small registration method instead of setting a private attribute directly.

Copilot uses AI. Check for mistakes.
# internal
_code_verifier: str | None = None
_state: str | None = None
Expand Down Expand Up @@ -990,6 +992,25 @@ def select_tenant(tenant_name: str) -> dict[str, Any]:
return {"status": "authenticated", "uipath_url": auth.uipath_url}


def _update_env_file(env_contents: dict[str, str]) -> None:
"""Merge *env_contents* into the CWD ``.env`` file.

New keys take priority; existing keys not in *env_contents* are preserved.
Mirrors ``uipath._utils._auth.update_env_file``.
"""
env_path = Path.cwd() / ".env"
if env_path.exists():
with open(env_path) as f:
for line in f:
if "=" in line:
key, value = line.strip().split("=", 1)
if key not in env_contents:
env_contents[key] = value
lines = [f"{key}={value}\n" for key, value in env_contents.items()]
with open(env_path, "w") as f:
f.writelines(lines)


Comment on lines +1002 to +1013
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_update_env_file rewrites .env using only key=value lines derived from env_contents, which drops existing comments/blank lines and can also accidentally treat comment lines containing = as keys. This is a behavior regression from the previous implementation that preserved formatting; consider preserving non-assignment lines (and ignoring commented assignments) or delegating to a shared env-file updater utility to avoid surprising edits to users’ .env files.

Suggested change
if env_path.exists():
with open(env_path) as f:
for line in f:
if "=" in line:
key, value = line.strip().split("=", 1)
if key not in env_contents:
env_contents[key] = value
lines = [f"{key}={value}\n" for key, value in env_contents.items()]
with open(env_path, "w") as f:
f.writelines(lines)
# Read existing .env lines (if any) so we can update in place
existing_lines: list[str] = []
if env_path.exists():
with open(env_path, encoding="utf-8") as f:
existing_lines = f.readlines()
# Work on a copy so we can track which keys still need to be appended
pending: dict[str, str] = dict(env_contents)
if existing_lines:
new_lines: list[str] = []
for line in existing_lines:
stripped = line.lstrip()
# Preserve comments and non-assignment lines verbatim
if stripped.startswith("#") or "=" not in stripped:
new_lines.append(line)
continue
key_part, _sep, _rest = stripped.partition("=")
key = key_part.strip()
# If parsing yields an empty or commented key, keep the line as-is
if not key or key.startswith("#"):
new_lines.append(line)
continue
# Update existing keys with new values when provided
if key in pending:
new_lines.append(f"{key}={pending.pop(key)}\n")
else:
new_lines.append(line)
# Append any remaining new keys at the end
if pending:
if new_lines and not new_lines[-1].endswith("\n"):
new_lines[-1] = new_lines[-1] + "\n"
for key, value in pending.items():
new_lines.append(f"{key}={value}\n")
lines_to_write = new_lines
else:
# No existing .env: just write the provided contents
lines_to_write = [f"{key}={value}\n" for key, value in pending.items()]
with open(env_path, "w", encoding="utf-8") as f:
f.writelines(lines_to_write)

Copilot uses AI. Check for mistakes.
def _finalize_tenant(auth: AuthState, tenant_name: str) -> None:
"""Write .env and os.environ with the resolved credentials."""
org_name = auth.organization.get("name", "")
Expand All @@ -1010,45 +1031,24 @@ def _finalize_tenant(auth: AuthState, tenant_name: str) -> None:
auth._last_org = dict(auth.organization)
auth._last_environment = auth.environment

# Update os.environ
os.environ["UIPATH_ACCESS_TOKEN"] = access_token
os.environ["UIPATH_URL"] = uipath_url
os.environ["UIPATH_TENANT_ID"] = tenant_id
os.environ["UIPATH_ORGANIZATION_ID"] = org_id

# Write/update .env file (preserving comments, blank lines, and ordering)
env_path = Path.cwd() / ".env"
lines: list[str] = []
updated_keys: set[str] = set()
new_values = {
"UIPATH_ACCESS_TOKEN": access_token,
"UIPATH_URL": uipath_url,
"UIPATH_TENANT_ID": tenant_id,
"UIPATH_ORGANIZATION_ID": org_id,
}

if env_path.exists():
with open(env_path) as f:
for raw_line in f:
stripped = raw_line.strip()
if "=" in stripped and not stripped.startswith("#"):
key = stripped.split("=", 1)[0]
if key in new_values:
lines.append(f"{key}={new_values[key]}\n")
updated_keys.add(key)
continue
lines.append(raw_line)

# Append any keys that weren't already in the file
for key, value in new_values.items():
if key not in updated_keys:
lines.append(f"{key}={value}\n")
# Write .env using the same approach as `uipath auth`
_update_env_file(
{
"UIPATH_ACCESS_TOKEN": access_token,
"UIPATH_URL": uipath_url,
"UIPATH_TENANT_ID": tenant_id,
"UIPATH_ORGANIZATION_ID": org_id,
}
)

with open(env_path, "w") as f:
f.writelines(lines)
# Reload .env into os.environ (same as CLI root: cwd + override)
load_dotenv(
dotenv_path=os.path.join(os.getcwd(), ".env"),
override=True,
)

# Reload all .env variables into os.environ
load_dotenv(override=True)
if auth._on_authenticated:
auth._on_authenticated()
Comment on lines +1050 to +1051
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling auth._on_authenticated() directly means any exception in the callback (or a failure to schedule its async work) will bubble out of _finalize_tenant and fail the login/tenant selection request. Consider wrapping the callback invocation in a try/except with logging, and if the callback schedules async work, make sure task exceptions are captured/logged as well.

Copilot uses AI. Check for mistakes.


def logout() -> None:
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.