Skip to content

fix: reload runtime factory after login#95

Open
cristipufu wants to merge 1 commit intomainfrom
fix/reload-factory-after-login
Open

fix: reload runtime factory after login#95
cristipufu wants to merge 1 commit intomainfrom
fix/reload-factory-after-login

Conversation

@cristipufu
Copy link
Member

Summary

  • The UiPath runtime caches credentials at init time (UiPathApiConfig). When the server starts unauthenticated and the user logs in, os.environ gets updated but the factory still holds stale credentials → 401s on evals/runs
  • After successful login, _finalize_tenant now triggers server.reload_factory() via an _on_authenticated callback, so the runtime is recreated with fresh credentials
  • Write .env using the same approach as uipath auth CLI (update_env_file pattern)
  • load_dotenv now uses explicit dotenv_path=cwd/.env + override=True, matching the CLI root
  • Bump version to 0.0.69

Test plan

  • Start dev server without credentials → try running evals (expect 401)
  • Login via UI → run evals again (should succeed without server restart)
  • Verify .env contains UIPATH_ACCESS_TOKEN, UIPATH_URL, UIPATH_TENANT_ID, UIPATH_ORGANIZATION_ID

🤖 Generated with Claude Code

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>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes stale UiPath runtime credentials after interactive login by reloading the runtime factory once authentication writes refreshed credentials to .env / os.environ, preventing 401s without requiring a server restart.

Changes:

  • Add an “on authenticated” hook to trigger server.reload_factory() after successful login/tenant finalization.
  • Change .env writing/loading behavior to match the CLI pattern (merge/update then load_dotenv(..., override=True) from cwd/.env).
  • Bump package version to 0.0.69.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

File Description
uv.lock Updates locked package version to 0.0.69.
src/uipath/dev/server/auth.py Adds .env merge/update helper, reloads dotenv explicitly from CWD, and invokes an authentication completion callback.
src/uipath/dev/server/app.py Wires auth completion callback to schedule a runtime factory reload after login.
pyproject.toml Bumps project version to 0.0.69.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +170 to +173
# 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())
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 +112 to +113
# Callback invoked after successful authentication
_on_authenticated: Any | None = None
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.
Comment on lines +1002 to +1013
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)


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.
Comment on lines +1050 to +1051
if auth._on_authenticated:
auth._on_authenticated()
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.
Comment on lines +173 to +174
asyncio.ensure_future(server.reload_factory())

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants