From 2a2a2a26c6c85de76287371ff978cd11e2c77c1e Mon Sep 17 00:00:00 2001 From: Chibi Vikram Date: Thu, 30 Apr 2026 22:15:24 -0700 Subject: [PATCH 1/9] feat(eval): honor UIPATH_AGENT_ID and CLOUD_USER_ID env vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Local-workspace eval runs distinguish two project IDs that the SDK had been collapsing into one: the *file-source project* (cloud project the worker fetches package files from) and the *logical agent* (the agent the user authored). For cloud projects the two are equal; for local workspaces they differ — file source is the per-run debug project's GUID, agent is the user's local agent UUID. When the worker runs under a service-account context (serverless), its JWT carries the service account's `sub` claim, not the real triggering user. Telemetry tagged from the JWT therefore attributes runs to the wrong identity. This change introduces three env vars the backend can set so the SDK tags telemetry with the right identities: - UIPATH_FILE_SOURCE_PROJECT_ID — preferred over UIPATH_PROJECT_ID for file-source semantics; falls back to UIPATH_PROJECT_ID so existing cloud deployments are unaffected. - UIPATH_AGENT_ID — the logical agent. Telemetry "AgentId" is now tagged with this when set, falling back to the file-source project for cloud runs. - UIPATH_CLOUD_USER_ID — the real user. Eval and CLI telemetry prefer this over the JWT sub claim, falling back to the JWT for older backends. The route URL the SDK uses for eval-run callbacks is unchanged (stays on file-source project) — backend-side ownership inheritance in CreateEvalRunAsync handles the AgentId/CloudUserId attribution end-to-end. This SDK change is the telemetry/observability half. 10 new tests in packages/uipath-platform/tests/common/ cover the fallback chain for each property. Existing CLI telemetry tests unchanged and still pass. Refs: AE-1396, PC-4407 --- .../src/uipath/platform/common/_config.py | 49 +++++++++- .../src/uipath/platform/common/constants.py | 16 ++++ .../tests/common/test_config_env_vars.py | 92 +++++++++++++++++++ .../src/uipath/_cli/_evals/_telemetry.py | 26 ++++-- packages/uipath/src/uipath/_cli/_telemetry.py | 30 +++--- 5 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 packages/uipath-platform/tests/common/test_config_env_vars.py diff --git a/packages/uipath-platform/src/uipath/platform/common/_config.py b/packages/uipath-platform/src/uipath/platform/common/_config.py index b656830f6..03c9cc0ad 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_config.py +++ b/packages/uipath-platform/src/uipath/platform/common/_config.py @@ -57,9 +57,54 @@ def config_file_name(self) -> str: @property def project_id(self) -> str | None: - from uipath.platform.common.constants import ENV_UIPATH_PROJECT_ID + # Prefer the explicit file-source name; fall back to the legacy alias so + # cloud projects and older runtimes continue to work unchanged. + from uipath.platform.common.constants import ( + ENV_UIPATH_FILE_SOURCE_PROJECT_ID, + ENV_UIPATH_PROJECT_ID, + ) + + return os.getenv(ENV_UIPATH_FILE_SOURCE_PROJECT_ID) or os.getenv( + ENV_UIPATH_PROJECT_ID, None + ) + + @property + def agent_id(self) -> str | None: + """The logical agent the user authored. + + For cloud projects this equals project_id. For local-workspace runs it + differs: project_id is the cloud debug project's GUID (file source), + while agent_id is the local agent UUID. Telemetry should prefer this + when tagging AgentId so dashboards group by the real agent rather than + the per-run debug project. + + Falls back to project_id when not explicitly set, so existing cloud + callers see no change. + """ + from uipath.platform.common.constants import ENV_UIPATH_AGENT_ID + + return os.getenv(ENV_UIPATH_AGENT_ID) or self.project_id + + @property + def cloud_user_id(self) -> str | None: + """The user who triggered the run. + + When the backend dispatches a worker under a service-account context, + the JWT in-flight has the service account's sub claim, not the real + user. The backend sets UIPATH_CLOUD_USER_ID so the SDK can tag traces + and telemetry with the right user. Returns None when unset (callers + should fall back to JWT claim extraction). + """ + from uipath.platform.common.constants import ENV_UIPATH_CLOUD_USER_ID + + return os.getenv(ENV_UIPATH_CLOUD_USER_ID, None) + + @property + def project_files_source(self) -> str | None: + """'Local' or 'Cloud' — where the worker's project files came from.""" + from uipath.platform.common.constants import ENV_UIPATH_PROJECT_FILES_SOURCE - return os.getenv(ENV_UIPATH_PROJECT_ID, None) + return os.getenv(ENV_UIPATH_PROJECT_FILES_SOURCE, None) @property def project_key(self) -> str | None: diff --git a/packages/uipath-platform/src/uipath/platform/common/constants.py b/packages/uipath-platform/src/uipath/platform/common/constants.py index 6184e844d..ef7bd9664 100644 --- a/packages/uipath-platform/src/uipath/platform/common/constants.py +++ b/packages/uipath-platform/src/uipath/platform/common/constants.py @@ -17,6 +17,22 @@ ENV_TELEMETRY_ENABLED = "UIPATH_TELEMETRY_ENABLED" ENV_TRACING_ENABLED = "UIPATH_TRACING_ENABLED" ENV_UIPATH_PROJECT_ID = "UIPATH_PROJECT_ID" +# The cloud project the worker fetches package files from. For cloud projects this +# equals the agent ID; for local-workspace runs it is the debug project ID returned +# by prepareForCustomDebug. Falls back to UIPATH_PROJECT_ID for backwards compat. +ENV_UIPATH_FILE_SOURCE_PROJECT_ID = "UIPATH_FILE_SOURCE_PROJECT_ID" +# The logical agent the user authored. For local-workspace runs this differs from +# UIPATH_FILE_SOURCE_PROJECT_ID (which is the cloud debug project's GUID). When +# present, eval-side telemetry tags AgentId with this value instead of the file +# source. Routing of API callbacks (eval-run create) still uses the file source +# project so the URL points at a project the backend can resolve. +ENV_UIPATH_AGENT_ID = "UIPATH_AGENT_ID" +# The user who triggered the job. Set by the backend when it dispatches a worker +# under a service-account context, so the SDK can tag telemetry with the real user +# rather than the service account from the JWT. +ENV_UIPATH_CLOUD_USER_ID = "UIPATH_CLOUD_USER_ID" +# "Local" or "Cloud" — the source of the project files the worker is using. +ENV_UIPATH_PROJECT_FILES_SOURCE = "UIPATH_PROJECT_FILES_SOURCE" ENV_PROJECT_KEY = "PROJECT_KEY" ENV_PROCESS_KEY = "UIPATH_PROCESS_KEY" ENV_UIPATH_PROCESS_UUID = "UIPATH_PROCESS_UUID" diff --git a/packages/uipath-platform/tests/common/test_config_env_vars.py b/packages/uipath-platform/tests/common/test_config_env_vars.py new file mode 100644 index 000000000..cdcc3f9be --- /dev/null +++ b/packages/uipath-platform/tests/common/test_config_env_vars.py @@ -0,0 +1,92 @@ +"""Tests for the env-var-aware properties on ConfigurationManager. + +Covers the file-source / agent / cloud-user split introduced for local-workspace +eval runs. Cloud-project runs continue to use the legacy single-id behaviour +(no new env vars set), and the tests assert that fallback explicitly. +""" + +import pytest + +from uipath.platform.common._config import UiPathConfig + + +@pytest.fixture(autouse=True) +def _clear_env(monkeypatch): + """Clear all env vars touched by these tests so each starts from a clean slate. + + ConfigurationManager is a singleton and its properties read os.environ on + every access, so setting/unsetting via monkeypatch is sufficient — no + instance reset needed. + """ + for var in ( + "UIPATH_PROJECT_ID", + "UIPATH_FILE_SOURCE_PROJECT_ID", + "UIPATH_AGENT_ID", + "UIPATH_CLOUD_USER_ID", + "UIPATH_PROJECT_FILES_SOURCE", + ): + monkeypatch.delenv(var, raising=False) + + +class TestProjectIdFallback: + """project_id prefers UIPATH_FILE_SOURCE_PROJECT_ID, falls back to UIPATH_PROJECT_ID.""" + + def test_returns_file_source_when_set(self, monkeypatch): + monkeypatch.setenv("UIPATH_FILE_SOURCE_PROJECT_ID", "file-source-id") + monkeypatch.setenv("UIPATH_PROJECT_ID", "legacy-id") + assert UiPathConfig.project_id == "file-source-id" + + def test_falls_back_to_legacy_when_file_source_unset(self, monkeypatch): + # Cloud projects today only set UIPATH_PROJECT_ID; that path must keep + # working so existing deployments aren't broken. + monkeypatch.setenv("UIPATH_PROJECT_ID", "legacy-id") + assert UiPathConfig.project_id == "legacy-id" + + def test_returns_none_when_neither_set(self): + assert UiPathConfig.project_id is None + + +class TestAgentId: + """agent_id is the logical agent. Distinct from project_id for local workspaces.""" + + def test_returns_explicit_agent_id_when_set(self, monkeypatch): + # Local-workspace eval scenario: file source is the cloud debug project, + # but AgentId is the user's local agent UUID. Telemetry should tag the + # logical agent so dashboards group by what the user authored. + monkeypatch.setenv("UIPATH_FILE_SOURCE_PROJECT_ID", "debug-project-guid") + monkeypatch.setenv("UIPATH_AGENT_ID", "real-agent-id") + assert UiPathConfig.agent_id == "real-agent-id" + + def test_falls_back_to_project_id_when_agent_id_unset(self, monkeypatch): + # Cloud-project scenario: only UIPATH_PROJECT_ID is set; agent_id must + # equal it so existing telemetry semantics are preserved. + monkeypatch.setenv("UIPATH_PROJECT_ID", "cloud-project-id") + assert UiPathConfig.agent_id == "cloud-project-id" + + def test_returns_none_when_neither_set(self): + assert UiPathConfig.agent_id is None + + +class TestCloudUserId: + """cloud_user_id reads UIPATH_CLOUD_USER_ID. Returns None when unset (callers + fall back to JWT extraction).""" + + def test_returns_value_when_set(self, monkeypatch): + monkeypatch.setenv("UIPATH_CLOUD_USER_ID", "user-guid") + assert UiPathConfig.cloud_user_id == "user-guid" + + def test_returns_none_when_unset(self): + # JWT fallback is a separate concern handled by call sites; the property + # itself returns None and lets the caller decide. + assert UiPathConfig.cloud_user_id is None + + +class TestProjectFilesSource: + """project_files_source surfaces 'Local' or 'Cloud' for telemetry tagging.""" + + def test_returns_value_when_set(self, monkeypatch): + monkeypatch.setenv("UIPATH_PROJECT_FILES_SOURCE", "Local") + assert UiPathConfig.project_files_source == "Local" + + def test_returns_none_when_unset(self): + assert UiPathConfig.project_files_source is None diff --git a/packages/uipath/src/uipath/_cli/_evals/_telemetry.py b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py index ad9549a6c..de9753d62 100644 --- a/packages/uipath/src/uipath/_cli/_evals/_telemetry.py +++ b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py @@ -308,22 +308,30 @@ def _enrich_properties(self, properties: dict[str, Any]) -> None: Args: properties: The properties dictionary to enrich. """ - # Add UiPath context + # Add UiPath context. ProjectId is the file-source project (cloud project + # the worker fetched files from). AgentId is the logical agent the user + # authored — equal to ProjectId for cloud runs, distinct for local-workspace + # runs where the file source is the per-run debug project's GUID. if UiPathConfig.project_id: properties["ProjectId"] = UiPathConfig.project_id - properties["AgentId"] = UiPathConfig.project_id + if UiPathConfig.agent_id: + properties["AgentId"] = UiPathConfig.agent_id # Get organization ID from UiPathConfig if UiPathConfig.organization_id: properties["CloudOrganizationId"] = UiPathConfig.organization_id - # Get CloudUserId from JWT token - try: - cloud_user_id = get_claim_from_token("sub") - if cloud_user_id: - properties["CloudUserId"] = cloud_user_id - except Exception: - pass # CloudUserId is optional + # CloudUserId: prefer the explicit env var the backend sets (it carries + # the real triggering user even when the worker runs under a service + # account). Fall back to the JWT sub claim for older backends/runtimes. + cloud_user_id = UiPathConfig.cloud_user_id + if not cloud_user_id: + try: + cloud_user_id = get_claim_from_token("sub") + except Exception: + cloud_user_id = None + if cloud_user_id: + properties["CloudUserId"] = cloud_user_id # Get tenant ID from environment tenant_id = os.getenv("UIPATH_TENANT_ID") diff --git a/packages/uipath/src/uipath/_cli/_telemetry.py b/packages/uipath/src/uipath/_cli/_telemetry.py index 7adc54410..f7836860c 100644 --- a/packages/uipath/src/uipath/_cli/_telemetry.py +++ b/packages/uipath/src/uipath/_cli/_telemetry.py @@ -41,10 +41,14 @@ def _enrich_properties(self, properties: Dict[str, Any]) -> None: Args: properties: The properties dictionary to enrich. """ - # Add UiPath context - project_key = _get_project_key() - if project_key: - properties["AgentId"] = project_key + # Add UiPath context. Prefer the explicit UIPATH_AGENT_ID env var (the + # logical agent the user authored) over the project_key / file-source + # project so telemetry groups runs by agent rather than by per-run + # debug project (matters for local-workspace evals where the two + # differ). + agent_id = UiPathConfig.agent_id or _get_project_key() + if agent_id: + properties["AgentId"] = agent_id # Get organization ID if UiPathConfig.organization_id: @@ -54,13 +58,17 @@ def _enrich_properties(self, properties: Dict[str, Any]) -> None: if UiPathConfig.tenant_id: properties["CloudTenantId"] = UiPathConfig.tenant_id - # Get CloudUserId from JWT token - try: - cloud_user_id = get_claim_from_token("sub") - if cloud_user_id: - properties["CloudUserId"] = cloud_user_id - except Exception: - pass + # CloudUserId: prefer the explicit env var the backend sets (carries + # the real triggering user even when the worker runs under a service + # account). Fall back to the JWT sub claim for older backends/runtimes. + cloud_user_id = UiPathConfig.cloud_user_id + if not cloud_user_id: + try: + cloud_user_id = get_claim_from_token("sub") + except Exception: + cloud_user_id = None + if cloud_user_id: + properties["CloudUserId"] = cloud_user_id properties["SessionId"] = "nosession" # Placeholder for session ID From 0fa134519c620265465732c80c082f2ddd334612 Mon Sep 17 00:00:00 2001 From: Chibi Vikram Date: Thu, 30 Apr 2026 22:20:33 -0700 Subject: [PATCH 2/9] chore: trim verbose comments per review --- .../src/uipath/platform/common/_config.py | 22 ------------- .../src/uipath/platform/common/constants.py | 12 ------- .../tests/common/test_config_env_vars.py | 31 ------------------- .../src/uipath/_cli/_evals/_telemetry.py | 8 ----- packages/uipath/src/uipath/_cli/_telemetry.py | 10 ------ 5 files changed, 83 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/common/_config.py b/packages/uipath-platform/src/uipath/platform/common/_config.py index 03c9cc0ad..c7f0ed69e 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_config.py +++ b/packages/uipath-platform/src/uipath/platform/common/_config.py @@ -57,8 +57,6 @@ def config_file_name(self) -> str: @property def project_id(self) -> str | None: - # Prefer the explicit file-source name; fall back to the legacy alias so - # cloud projects and older runtimes continue to work unchanged. from uipath.platform.common.constants import ( ENV_UIPATH_FILE_SOURCE_PROJECT_ID, ENV_UIPATH_PROJECT_ID, @@ -70,38 +68,18 @@ def project_id(self) -> str | None: @property def agent_id(self) -> str | None: - """The logical agent the user authored. - - For cloud projects this equals project_id. For local-workspace runs it - differs: project_id is the cloud debug project's GUID (file source), - while agent_id is the local agent UUID. Telemetry should prefer this - when tagging AgentId so dashboards group by the real agent rather than - the per-run debug project. - - Falls back to project_id when not explicitly set, so existing cloud - callers see no change. - """ from uipath.platform.common.constants import ENV_UIPATH_AGENT_ID return os.getenv(ENV_UIPATH_AGENT_ID) or self.project_id @property def cloud_user_id(self) -> str | None: - """The user who triggered the run. - - When the backend dispatches a worker under a service-account context, - the JWT in-flight has the service account's sub claim, not the real - user. The backend sets UIPATH_CLOUD_USER_ID so the SDK can tag traces - and telemetry with the right user. Returns None when unset (callers - should fall back to JWT claim extraction). - """ from uipath.platform.common.constants import ENV_UIPATH_CLOUD_USER_ID return os.getenv(ENV_UIPATH_CLOUD_USER_ID, None) @property def project_files_source(self) -> str | None: - """'Local' or 'Cloud' — where the worker's project files came from.""" from uipath.platform.common.constants import ENV_UIPATH_PROJECT_FILES_SOURCE return os.getenv(ENV_UIPATH_PROJECT_FILES_SOURCE, None) diff --git a/packages/uipath-platform/src/uipath/platform/common/constants.py b/packages/uipath-platform/src/uipath/platform/common/constants.py index ef7bd9664..cf7b2df19 100644 --- a/packages/uipath-platform/src/uipath/platform/common/constants.py +++ b/packages/uipath-platform/src/uipath/platform/common/constants.py @@ -17,21 +17,9 @@ ENV_TELEMETRY_ENABLED = "UIPATH_TELEMETRY_ENABLED" ENV_TRACING_ENABLED = "UIPATH_TRACING_ENABLED" ENV_UIPATH_PROJECT_ID = "UIPATH_PROJECT_ID" -# The cloud project the worker fetches package files from. For cloud projects this -# equals the agent ID; for local-workspace runs it is the debug project ID returned -# by prepareForCustomDebug. Falls back to UIPATH_PROJECT_ID for backwards compat. ENV_UIPATH_FILE_SOURCE_PROJECT_ID = "UIPATH_FILE_SOURCE_PROJECT_ID" -# The logical agent the user authored. For local-workspace runs this differs from -# UIPATH_FILE_SOURCE_PROJECT_ID (which is the cloud debug project's GUID). When -# present, eval-side telemetry tags AgentId with this value instead of the file -# source. Routing of API callbacks (eval-run create) still uses the file source -# project so the URL points at a project the backend can resolve. ENV_UIPATH_AGENT_ID = "UIPATH_AGENT_ID" -# The user who triggered the job. Set by the backend when it dispatches a worker -# under a service-account context, so the SDK can tag telemetry with the real user -# rather than the service account from the JWT. ENV_UIPATH_CLOUD_USER_ID = "UIPATH_CLOUD_USER_ID" -# "Local" or "Cloud" — the source of the project files the worker is using. ENV_UIPATH_PROJECT_FILES_SOURCE = "UIPATH_PROJECT_FILES_SOURCE" ENV_PROJECT_KEY = "PROJECT_KEY" ENV_PROCESS_KEY = "UIPATH_PROCESS_KEY" diff --git a/packages/uipath-platform/tests/common/test_config_env_vars.py b/packages/uipath-platform/tests/common/test_config_env_vars.py index cdcc3f9be..ea301626e 100644 --- a/packages/uipath-platform/tests/common/test_config_env_vars.py +++ b/packages/uipath-platform/tests/common/test_config_env_vars.py @@ -1,10 +1,3 @@ -"""Tests for the env-var-aware properties on ConfigurationManager. - -Covers the file-source / agent / cloud-user split introduced for local-workspace -eval runs. Cloud-project runs continue to use the legacy single-id behaviour -(no new env vars set), and the tests assert that fallback explicitly. -""" - import pytest from uipath.platform.common._config import UiPathConfig @@ -12,12 +5,6 @@ @pytest.fixture(autouse=True) def _clear_env(monkeypatch): - """Clear all env vars touched by these tests so each starts from a clean slate. - - ConfigurationManager is a singleton and its properties read os.environ on - every access, so setting/unsetting via monkeypatch is sufficient — no - instance reset needed. - """ for var in ( "UIPATH_PROJECT_ID", "UIPATH_FILE_SOURCE_PROJECT_ID", @@ -29,16 +16,12 @@ def _clear_env(monkeypatch): class TestProjectIdFallback: - """project_id prefers UIPATH_FILE_SOURCE_PROJECT_ID, falls back to UIPATH_PROJECT_ID.""" - def test_returns_file_source_when_set(self, monkeypatch): monkeypatch.setenv("UIPATH_FILE_SOURCE_PROJECT_ID", "file-source-id") monkeypatch.setenv("UIPATH_PROJECT_ID", "legacy-id") assert UiPathConfig.project_id == "file-source-id" def test_falls_back_to_legacy_when_file_source_unset(self, monkeypatch): - # Cloud projects today only set UIPATH_PROJECT_ID; that path must keep - # working so existing deployments aren't broken. monkeypatch.setenv("UIPATH_PROJECT_ID", "legacy-id") assert UiPathConfig.project_id == "legacy-id" @@ -47,19 +30,12 @@ def test_returns_none_when_neither_set(self): class TestAgentId: - """agent_id is the logical agent. Distinct from project_id for local workspaces.""" - def test_returns_explicit_agent_id_when_set(self, monkeypatch): - # Local-workspace eval scenario: file source is the cloud debug project, - # but AgentId is the user's local agent UUID. Telemetry should tag the - # logical agent so dashboards group by what the user authored. monkeypatch.setenv("UIPATH_FILE_SOURCE_PROJECT_ID", "debug-project-guid") monkeypatch.setenv("UIPATH_AGENT_ID", "real-agent-id") assert UiPathConfig.agent_id == "real-agent-id" def test_falls_back_to_project_id_when_agent_id_unset(self, monkeypatch): - # Cloud-project scenario: only UIPATH_PROJECT_ID is set; agent_id must - # equal it so existing telemetry semantics are preserved. monkeypatch.setenv("UIPATH_PROJECT_ID", "cloud-project-id") assert UiPathConfig.agent_id == "cloud-project-id" @@ -68,22 +44,15 @@ def test_returns_none_when_neither_set(self): class TestCloudUserId: - """cloud_user_id reads UIPATH_CLOUD_USER_ID. Returns None when unset (callers - fall back to JWT extraction).""" - def test_returns_value_when_set(self, monkeypatch): monkeypatch.setenv("UIPATH_CLOUD_USER_ID", "user-guid") assert UiPathConfig.cloud_user_id == "user-guid" def test_returns_none_when_unset(self): - # JWT fallback is a separate concern handled by call sites; the property - # itself returns None and lets the caller decide. assert UiPathConfig.cloud_user_id is None class TestProjectFilesSource: - """project_files_source surfaces 'Local' or 'Cloud' for telemetry tagging.""" - def test_returns_value_when_set(self, monkeypatch): monkeypatch.setenv("UIPATH_PROJECT_FILES_SOURCE", "Local") assert UiPathConfig.project_files_source == "Local" diff --git a/packages/uipath/src/uipath/_cli/_evals/_telemetry.py b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py index de9753d62..9c9d0df62 100644 --- a/packages/uipath/src/uipath/_cli/_evals/_telemetry.py +++ b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py @@ -308,22 +308,14 @@ def _enrich_properties(self, properties: dict[str, Any]) -> None: Args: properties: The properties dictionary to enrich. """ - # Add UiPath context. ProjectId is the file-source project (cloud project - # the worker fetched files from). AgentId is the logical agent the user - # authored — equal to ProjectId for cloud runs, distinct for local-workspace - # runs where the file source is the per-run debug project's GUID. if UiPathConfig.project_id: properties["ProjectId"] = UiPathConfig.project_id if UiPathConfig.agent_id: properties["AgentId"] = UiPathConfig.agent_id - # Get organization ID from UiPathConfig if UiPathConfig.organization_id: properties["CloudOrganizationId"] = UiPathConfig.organization_id - # CloudUserId: prefer the explicit env var the backend sets (it carries - # the real triggering user even when the worker runs under a service - # account). Fall back to the JWT sub claim for older backends/runtimes. cloud_user_id = UiPathConfig.cloud_user_id if not cloud_user_id: try: diff --git a/packages/uipath/src/uipath/_cli/_telemetry.py b/packages/uipath/src/uipath/_cli/_telemetry.py index f7836860c..0ced2bf90 100644 --- a/packages/uipath/src/uipath/_cli/_telemetry.py +++ b/packages/uipath/src/uipath/_cli/_telemetry.py @@ -41,26 +41,16 @@ def _enrich_properties(self, properties: Dict[str, Any]) -> None: Args: properties: The properties dictionary to enrich. """ - # Add UiPath context. Prefer the explicit UIPATH_AGENT_ID env var (the - # logical agent the user authored) over the project_key / file-source - # project so telemetry groups runs by agent rather than by per-run - # debug project (matters for local-workspace evals where the two - # differ). agent_id = UiPathConfig.agent_id or _get_project_key() if agent_id: properties["AgentId"] = agent_id - # Get organization ID if UiPathConfig.organization_id: properties["CloudOrganizationId"] = UiPathConfig.organization_id - # Get tenant ID if UiPathConfig.tenant_id: properties["CloudTenantId"] = UiPathConfig.tenant_id - # CloudUserId: prefer the explicit env var the backend sets (carries - # the real triggering user even when the worker runs under a service - # account). Fall back to the JWT sub claim for older backends/runtimes. cloud_user_id = UiPathConfig.cloud_user_id if not cloud_user_id: try: From 3f1c8a24c581fd6b853d20bb26ee122df5392c0f Mon Sep 17 00:00:00 2001 From: Chibi Vikram Date: Thu, 30 Apr 2026 23:44:08 -0700 Subject: [PATCH 3/9] chore: bump uipath to 2.10.61 and uipath-platform to 0.1.43 --- packages/uipath-platform/pyproject.toml | 2 +- packages/uipath/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index c52b917eb..989952eb3 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.42" +version = "0.1.43" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 406c919af..b468a3db3 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.10.60" +version = "2.10.61" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" From bdf0ee39a0bd776b1195c56036e441b6429f747e Mon Sep 17 00:00:00 2001 From: Chibi Vikram Date: Thu, 30 Apr 2026 23:46:38 -0700 Subject: [PATCH 4/9] chore: drop residual low-value comments --- packages/uipath/src/uipath/_cli/_evals/_telemetry.py | 2 -- packages/uipath/src/uipath/_cli/_telemetry.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/uipath/src/uipath/_cli/_evals/_telemetry.py b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py index 9c9d0df62..e28186576 100644 --- a/packages/uipath/src/uipath/_cli/_evals/_telemetry.py +++ b/packages/uipath/src/uipath/_cli/_evals/_telemetry.py @@ -325,11 +325,9 @@ def _enrich_properties(self, properties: dict[str, Any]) -> None: if cloud_user_id: properties["CloudUserId"] = cloud_user_id - # Get tenant ID from environment tenant_id = os.getenv("UIPATH_TENANT_ID") if tenant_id: properties["TenantId"] = tenant_id - # Add source identifier properties["Source"] = "uipath-python-cli" properties["ApplicationName"] = "UiPath.Eval" diff --git a/packages/uipath/src/uipath/_cli/_telemetry.py b/packages/uipath/src/uipath/_cli/_telemetry.py index 0ced2bf90..7f0102235 100644 --- a/packages/uipath/src/uipath/_cli/_telemetry.py +++ b/packages/uipath/src/uipath/_cli/_telemetry.py @@ -60,7 +60,7 @@ def _enrich_properties(self, properties: Dict[str, Any]) -> None: if cloud_user_id: properties["CloudUserId"] = cloud_user_id - properties["SessionId"] = "nosession" # Placeholder for session ID + properties["SessionId"] = "nosession" try: properties["SDKVersion"] = version("uipath") @@ -69,7 +69,6 @@ def _enrich_properties(self, properties: Dict[str, Any]) -> None: properties["IsGithubCI"] = bool(os.getenv("GITHUB_ACTIONS")) - # Add source identifier properties["Source"] = "uipath-python-cli" properties["ApplicationName"] = "UiPath.AgentCli" From db26ce215fff0fbdbc1ef70f56c5d07a9a0b5820 Mon Sep 17 00:00:00 2001 From: Chibi Vikram Date: Thu, 30 Apr 2026 23:52:04 -0700 Subject: [PATCH 5/9] fix(eval): close two adversarial-review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Pin uipath-platform>=0.1.43 in uipath/pyproject.toml. The new UiPathConfig.agent_id / cloud_user_id properties only exist after the 0.1.43 bump; the old >=0.1.42 constraint allowed a skewed install where uipath==2.10.61 calls AttributeError-raising properties. 2. Restrict the CLI telemetry AgentId override to the explicit UIPATH_AGENT_ID env var instead of UiPathConfig.agent_id (which falls back to project_id). Prevents silent regression of non-eval CLI contexts that set UIPATH_PROJECT_ID — those continue to tag AgentId from PROJECT_KEY as before. Refs: AE-1396 --- packages/uipath/pyproject.toml | 2 +- packages/uipath/src/uipath/_cli/_telemetry.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index b468a3db3..b48c898b5 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uipath-core>=0.5.8, <0.6.0", "uipath-runtime>=0.10.1, <0.11.0", - "uipath-platform>=0.1.42, <0.2.0", + "uipath-platform>=0.1.43, <0.2.0", "click>=8.3.1", "httpx>=0.28.1", "pyjwt>=2.10.1", diff --git a/packages/uipath/src/uipath/_cli/_telemetry.py b/packages/uipath/src/uipath/_cli/_telemetry.py index 7f0102235..d245076a4 100644 --- a/packages/uipath/src/uipath/_cli/_telemetry.py +++ b/packages/uipath/src/uipath/_cli/_telemetry.py @@ -41,7 +41,7 @@ def _enrich_properties(self, properties: Dict[str, Any]) -> None: Args: properties: The properties dictionary to enrich. """ - agent_id = UiPathConfig.agent_id or _get_project_key() + agent_id = os.getenv("UIPATH_AGENT_ID") or _get_project_key() if agent_id: properties["AgentId"] = agent_id From fedbfa28697391f19ba51cfce9c4dc5c7cb2850a Mon Sep 17 00:00:00 2001 From: Chibi Vikram Date: Fri, 1 May 2026 00:00:52 -0700 Subject: [PATCH 6/9] chore: regenerate uv.lock after version bumps --- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 8695a33f1..66c8eb62a 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -1088,7 +1088,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.42" +version = "0.1.43" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index d59970eea..2eacfed8a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2543,7 +2543,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.10.60" +version = "2.10.61" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.42" +version = "0.1.43" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, From 5027a3f71678fb074e266eb43067dbabef0f7020 Mon Sep 17 00:00:00 2001 From: Chibi Vikram Date: Fri, 1 May 2026 12:39:30 -0700 Subject: [PATCH 7/9] fix(eval): route eval-run/eval-set-run API calls by AgentId The SDK's StudioWebProgressReporter was using UIPATH_PROJECT_ID for both the API URL (/api/execution/agents/{id}/evalRun) and the file fetch context. For local-workspace runs those are different IDs: file source is the cloud debug project's GUID, agent is the user's logical agent. Honoring the AgentId in API calls so the route URL reflects the real logical agent. File fetching (UiPathConfig.project_id, used by StudioClient/ResourceOverwritesContext in cli_eval.py) keeps using the file-source project, since that is where the worker's package files live. Changes: - StudioWebProgressReporter now exposes self._agent_id (UIPATH_AGENT_ID with fallback to self._project_id). Every API URL construction and the agentId payload field now use _agent_id. - self._project_id is preserved for the file-source semantic (env-var fallback for AgentId resolution); no remaining read sites in the reporter, but kept as the explicit "if file source ever needs callbacks" hook. Tests added in TestAgentIdRouting (test_progress_reporter.py): - _agent_id used in URL when both UIPATH_AGENT_ID and UIPATH_PROJECT_ID set - agentId payload field carries _agent_id, not file source - Falls back to project_id (file source) when UIPATH_AGENT_ID unset 1826 reporter tests pass (3 new). Refs: AE-1396 --- .../uipath/_cli/_evals/_progress_reporter.py | 19 ++--- .../tests/cli/eval/test_progress_reporter.py | 81 ++++++++++++++++++- 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py b/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py index 7c5114516..9cb3dd5a7 100644 --- a/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py +++ b/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py @@ -102,7 +102,8 @@ def __init__(self): self._console = console_logger self._rich_console = Console() self._project_id = os.getenv("UIPATH_PROJECT_ID", None) - if not self._project_id: + self._agent_id = os.getenv("UIPATH_AGENT_ID") or self._project_id + if not self._agent_id: logger.warning( "Cannot report data to StudioWeb. Please set UIPATH_PROJECT_ID." ) @@ -1133,7 +1134,7 @@ def _update_eval_run_spec( return RequestSpec( method="PUT", endpoint=Endpoint( - f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun" + f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun" ), json=payload, headers=self._tenant_header(), @@ -1181,7 +1182,7 @@ def _update_coded_eval_run_spec( return RequestSpec( method="PUT", endpoint=Endpoint( - f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun" + f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun" ), json=payload, headers=self._tenant_header(), @@ -1253,7 +1254,7 @@ def _create_eval_run_spec( return RequestSpec( method="POST", endpoint=Endpoint( - f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalRun" + f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalRun" ), json=payload, headers=self._tenant_header(), @@ -1283,7 +1284,7 @@ def _create_eval_set_run_spec( eval_set_id_value = str(uuid.uuid5(uuid.NAMESPACE_DNS, eval_set_id)) inner_payload: dict[str, Any] = { - "agentId": self._project_id, + "agentId": self._agent_id, "evalSetId": eval_set_id_value, "agentSnapshot": agent_snapshot.model_dump(by_alias=True), # Backend expects integer status @@ -1309,7 +1310,7 @@ def _create_eval_set_run_spec( return RequestSpec( method="POST", endpoint=Endpoint( - f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalSetRun" + f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalSetRun" ), json=payload, headers=self._tenant_header(), @@ -1374,7 +1375,7 @@ def _update_eval_set_run_spec( return RequestSpec( method="PUT", endpoint=Endpoint( - f"{self._get_endpoint_prefix()}execution/agents/{self._project_id}/{endpoint_suffix}evalSetRun" + f"{self._get_endpoint_prefix()}execution/agents/{self._agent_id}/{endpoint_suffix}evalSetRun" ), json=payload, headers=self._tenant_header(), @@ -1406,12 +1407,12 @@ def _get_eval_runs_spec( if is_coded: endpoint_path = ( - f"{prefix}execution/agents/{self._project_id}/coded/" + f"{prefix}execution/agents/{self._agent_id}/coded/" f"evalSets/{eval_set_id}/evalSetRuns/{eval_set_run_id}/evalRuns" ) else: endpoint_path = ( - f"{prefix}execution/agents/{self._project_id}/" + f"{prefix}execution/agents/{self._agent_id}/" f"evalSets/{eval_set_id}/evalSetRuns/{eval_set_run_id}/evalRuns" ) diff --git a/packages/uipath/tests/cli/eval/test_progress_reporter.py b/packages/uipath/tests/cli/eval/test_progress_reporter.py index 87919c2b3..0a5dd2c70 100644 --- a/packages/uipath/tests/cli/eval/test_progress_reporter.py +++ b/packages/uipath/tests/cli/eval/test_progress_reporter.py @@ -927,4 +927,83 @@ def test_build_evaluator_snapshot_skips_non_string_model(self, progress_reporter snapshot = progress_reporter._build_evaluator_snapshot(evaluator) assert snapshot["prompt"] == "Evaluate this" - assert "model" not in snapshot + + +class TestAgentIdRouting: + """Eval-set/eval-run API URLs route by AgentId, not file-source project. + + For local-workspace eval runs the file-source project (UIPATH_PROJECT_ID, + typically the cloud debug project's GUID) differs from the logical agent + (UIPATH_AGENT_ID). The route URL must reflect the logical agent so backend + auth/ownership/telemetry don't see the per-run debug project as the agent. + File fetching (UiPathConfig.project_id) is unaffected. + """ + + def _make_reporter(self, monkeypatch, project_id, agent_id): + monkeypatch.setenv("UIPATH_URL", "https://test.uipath.com") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "test-token") + monkeypatch.setenv("UIPATH_TENANT_ID", "test-tenant-id") + if project_id is not None: + monkeypatch.setenv("UIPATH_PROJECT_ID", project_id) + else: + monkeypatch.delenv("UIPATH_PROJECT_ID", raising=False) + if agent_id is not None: + monkeypatch.setenv("UIPATH_AGENT_ID", agent_id) + else: + monkeypatch.delenv("UIPATH_AGENT_ID", raising=False) + return StudioWebProgressReporter() + + def test_agent_id_used_in_url_when_both_set(self, monkeypatch): + reporter = self._make_reporter( + monkeypatch, project_id="debug-project-guid", agent_id="real-agent-id" + ) + assert reporter._agent_id == "real-agent-id" + assert reporter._project_id == "debug-project-guid" + + from uipath._cli._evals._progress_reporter import StudioWebAgentSnapshot + + spec = reporter._create_eval_set_run_spec( + eval_set_id="test-eval-set", + agent_snapshot=StudioWebAgentSnapshot( + input_schema={"type": "object"}, output_schema={"type": "object"} + ), + no_of_evals=1, + is_coded=False, + ) + assert "/agents/real-agent-id/" in spec.endpoint + assert "/agents/debug-project-guid/" not in spec.endpoint + + def test_agent_id_in_eval_set_run_payload(self, monkeypatch): + reporter = self._make_reporter( + monkeypatch, project_id="debug-project-guid", agent_id="real-agent-id" + ) + + from uipath._cli._evals._progress_reporter import StudioWebAgentSnapshot + + spec = reporter._create_eval_set_run_spec( + eval_set_id="test-eval-set", + agent_snapshot=StudioWebAgentSnapshot( + input_schema={"type": "object"}, output_schema={"type": "object"} + ), + no_of_evals=1, + is_coded=False, + ) + assert spec.json["agentId"] == "real-agent-id" + + def test_falls_back_to_project_id_when_agent_id_unset(self, monkeypatch): + reporter = self._make_reporter( + monkeypatch, project_id="cloud-project-id", agent_id=None + ) + assert reporter._agent_id == "cloud-project-id" + + from uipath._cli._evals._progress_reporter import StudioWebAgentSnapshot + + spec = reporter._create_eval_set_run_spec( + eval_set_id="test-eval-set", + agent_snapshot=StudioWebAgentSnapshot( + input_schema={"type": "object"}, output_schema={"type": "object"} + ), + no_of_evals=1, + is_coded=False, + ) + assert "/agents/cloud-project-id/" in spec.endpoint From 81bf325c52422492757ef82ead3062b198ee9d13 Mon Sep 17 00:00:00 2001 From: Chibi Vikram Date: Fri, 1 May 2026 14:02:05 -0700 Subject: [PATCH 8/9] fix(eval): propagate projectFilesSource on POST/PUT/GET so rows match UI filter projectFilesSource is the Local/Cloud discriminator the UI filters by (`?projectFilesSource=1` for local-workspace runs), not a project ID. Until now the SDK didn't send it on the eval-run / eval-set-run callbacks, so the backend created every worker-spawned row defaulting to Cloud and the UI's local-runs filter never matched them. Reads UIPATH_PROJECT_FILES_SOURCE (string Local/Cloud) once at reporter construction and maps it to the backend's enum integer (Local=1, Cloud=0). Adds it to: POST eval set run payload POST eval run payload PUT eval run (legacy) payload PUT eval run (coded) payload PUT eval set run payload GET eval runs (resume lookup) query parameter When the env var is unset (cloud-project case or older backend that doesn't emit it) the field is omitted from payloads/params, preserving existing behaviour. 14 new tests in TestProjectFilesSourcePropagation covering the parametrized env-var resolution (Local/Cloud/numeric/garbage), each of the six request shapes carries the right value, and absence-omits-field. 1840 reporter tests pass. Refs: AE-1396 --- .../uipath/_cli/_evals/_progress_reporter.py | 40 +++++- .../tests/cli/eval/test_progress_reporter.py | 124 ++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) diff --git a/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py b/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py index 9cb3dd5a7..fd4849076 100644 --- a/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py +++ b/packages/uipath/src/uipath/_cli/_evals/_progress_reporter.py @@ -108,6 +108,12 @@ def __init__(self): "Cannot report data to StudioWeb. Please set UIPATH_PROJECT_ID." ) + # Map UIPATH_PROJECT_FILES_SOURCE (Local/Cloud) to the backend's + # ProjectFilesSource enum integer. Without this every row the worker + # creates lands as Cloud, and the UI's `?projectFilesSource=1` filter + # never matches local-workspace runs. + self._project_files_source = self._resolve_project_files_source() + self.eval_set_ids: dict[str, str] = {} # Track eval_set_id per execution self.eval_set_run_ids: dict[str, str] = {} self.evaluators: dict[str, Any] = {} @@ -1090,6 +1096,29 @@ def _collect_coded_results( evaluator_runs.append(evaluator_run) return evaluator_runs, evaluator_scores_list + @staticmethod + def _resolve_project_files_source() -> int | None: + raw = os.getenv("UIPATH_PROJECT_FILES_SOURCE") + if not raw: + return None + normalized = raw.strip().lower() + if normalized == "local": + return 1 + if normalized == "cloud": + return 0 + try: + return int(normalized) + except ValueError: + logger.warning( + f"Unrecognized UIPATH_PROJECT_FILES_SOURCE value: {raw!r}; ignoring." + ) + return None + + def _project_files_source_field(self) -> dict[str, int]: + if self._project_files_source is None: + return {} + return {"projectFilesSource": self._project_files_source} + def _update_eval_run_spec( self, assertion_runs: list[dict[str, Any]], @@ -1116,6 +1145,7 @@ def _update_eval_run_spec( }, "completionMetrics": {"duration": int(execution_time * 1000)}, "assertionRuns": assertion_runs, + **self._project_files_source_field(), } # Legacy backend expects payload wrapped in "request" field @@ -1167,6 +1197,7 @@ def _update_coded_eval_run_spec( }, "completionMetrics": {"duration": int(execution_time * 1000)}, "evaluatorRuns": evaluator_runs, + **self._project_files_source_field(), } # Log the payload for debugging coded eval run updates @@ -1236,6 +1267,7 @@ def _create_eval_run_spec( "evalSnapshot": eval_snapshot, # Backend expects integer status "status": EvaluationStatus.IN_PROGRESS.value, + **self._project_files_source_field(), } # Legacy backend expects payload wrapped in "request" field @@ -1292,6 +1324,7 @@ def _create_eval_set_run_spec( "numberOfEvalsExecuted": no_of_evals, # Source is required by the backend (0 = coded SDK) "source": 0, + **self._project_files_source_field(), } # Both coded and legacy send payload directly at root level @@ -1354,6 +1387,7 @@ def _update_eval_set_run_spec( # Backend expects integer status "status": status.value, "evaluatorScores": evaluator_scores_list, + **self._project_files_source_field(), } # Legacy backend expects payload wrapped in "request" field @@ -1421,10 +1455,14 @@ def _get_eval_runs_spec( f"eval_set_run_id={eval_set_run_id}, evaluation_id={evaluation_id}, coded={is_coded}" ) + # The backend's listing endpoint filters by projectFilesSource + + # cloudUserId so the UI only shows the caller's local rows. Mirror + # that here so resume lookups match the row written by the same + # worker session. return RequestSpec( method="GET", endpoint=Endpoint(endpoint_path), - params={}, # No query params needed - evalSetRunId is in the path + params=self._project_files_source_field(), headers=self._tenant_header(), ) diff --git a/packages/uipath/tests/cli/eval/test_progress_reporter.py b/packages/uipath/tests/cli/eval/test_progress_reporter.py index 0a5dd2c70..1fd00bf12 100644 --- a/packages/uipath/tests/cli/eval/test_progress_reporter.py +++ b/packages/uipath/tests/cli/eval/test_progress_reporter.py @@ -1007,3 +1007,127 @@ def test_falls_back_to_project_id_when_agent_id_unset(self, monkeypatch): is_coded=False, ) assert "/agents/cloud-project-id/" in spec.endpoint + + +class TestProjectFilesSourcePropagation: + """Reporter must propagate UIPATH_PROJECT_FILES_SOURCE to backend rows. + + Backend filters listings by `projectFilesSource` (Local=1, Cloud=0). Without + the SDK setting it on POST/PUT payloads and GET query params, every row + lands as Cloud and the UI's `?projectFilesSource=1` filter never matches + local-workspace runs. + """ + + def _make_reporter(self, monkeypatch, project_files_source): + monkeypatch.setenv("UIPATH_URL", "https://test.uipath.com") + monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "test-token") + monkeypatch.setenv("UIPATH_TENANT_ID", "test-tenant-id") + monkeypatch.setenv("UIPATH_PROJECT_ID", "test-project-id") + if project_files_source is not None: + monkeypatch.setenv("UIPATH_PROJECT_FILES_SOURCE", project_files_source) + else: + monkeypatch.delenv("UIPATH_PROJECT_FILES_SOURCE", raising=False) + return StudioWebProgressReporter() + + @pytest.mark.parametrize( + "raw,expected", + [("Local", 1), ("local", 1), ("Cloud", 0), ("cloud", 0), ("1", 1), ("0", 0)], + ) + def test_resolves_env_var_to_int(self, monkeypatch, raw, expected): + reporter = self._make_reporter(monkeypatch, raw) + assert reporter._project_files_source == expected + + def test_returns_none_when_unset_or_garbage(self, monkeypatch): + reporter = self._make_reporter(monkeypatch, None) + assert reporter._project_files_source is None + reporter2 = self._make_reporter(monkeypatch, "Banana") + assert reporter2._project_files_source is None + + def test_post_eval_set_run_payload_carries_source(self, monkeypatch): + from uipath._cli._evals._progress_reporter import StudioWebAgentSnapshot + + reporter = self._make_reporter(monkeypatch, "Local") + spec = reporter._create_eval_set_run_spec( + eval_set_id="test-eval-set", + agent_snapshot=StudioWebAgentSnapshot( + input_schema={"type": "object"}, output_schema={"type": "object"} + ), + no_of_evals=1, + is_coded=False, + ) + assert spec.json["projectFilesSource"] == 1 + + def test_post_eval_run_payload_carries_source(self, monkeypatch): + from uipath.eval.models.evaluation_set import EvaluationItem + + reporter = self._make_reporter(monkeypatch, "Local") + item = EvaluationItem( + id="11111111-1111-1111-1111-111111111111", + name="t", + inputs={}, + evaluation_criterias={}, + ) + spec = reporter._create_eval_run_spec( + eval_item=item, eval_set_run_id="run-1", is_coded=False + ) + assert spec.json["projectFilesSource"] == 1 + + def test_put_eval_run_payload_carries_source(self, monkeypatch): + reporter = self._make_reporter(monkeypatch, "Local") + spec = reporter._update_eval_run_spec( + assertion_runs=[], + evaluator_scores=[], + eval_run_id="run-1", + actual_output={}, + execution_time=1.0, + success=True, + is_coded=False, + ) + assert spec.json["projectFilesSource"] == 1 + + def test_put_coded_eval_run_payload_carries_source(self, monkeypatch): + reporter = self._make_reporter(monkeypatch, "Local") + spec = reporter._update_coded_eval_run_spec( + evaluator_runs=[], + evaluator_scores=[], + eval_run_id="run-1", + actual_output={}, + execution_time=1.0, + success=True, + is_coded=True, + ) + assert spec.json["projectFilesSource"] == 1 + + def test_put_eval_set_run_payload_carries_source(self, monkeypatch): + reporter = self._make_reporter(monkeypatch, "Local") + spec = reporter._update_eval_set_run_spec( + eval_set_run_id="set-run-1", + evaluator_scores={}, + is_coded=False, + success=True, + ) + assert spec.json["projectFilesSource"] == 1 + + def test_get_eval_runs_query_carries_source(self, monkeypatch): + reporter = self._make_reporter(monkeypatch, "Local") + spec = reporter._get_eval_runs_spec( + eval_set_id="set-1", + eval_set_run_id="run-1", + evaluation_id=None, + is_coded=False, + ) + assert spec.params == {"projectFilesSource": 1} + + def test_unset_source_omits_field_from_payloads(self, monkeypatch): + from uipath._cli._evals._progress_reporter import StudioWebAgentSnapshot + + reporter = self._make_reporter(monkeypatch, None) + spec = reporter._create_eval_set_run_spec( + eval_set_id="test-eval-set", + agent_snapshot=StudioWebAgentSnapshot( + input_schema={"type": "object"}, output_schema={"type": "object"} + ), + no_of_evals=1, + is_coded=False, + ) + assert "projectFilesSource" not in spec.json From 2728e521e47a133eacba6791ef3e50fdf1c8f2be Mon Sep 17 00:00:00 2001 From: Chibi Vikram Date: Fri, 1 May 2026 14:15:54 -0700 Subject: [PATCH 9/9] chore(eval): drop redundant UIPATH_FILE_SOURCE_PROJECT_ID env var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UIPATH_PROJECT_ID already carries the file-source semantic (cloud project the worker fetches files from). UIPATH_FILE_SOURCE_PROJECT_ID was an alias that added a third name for the same value without adding meaning. UiPathConfig.project_id now reads UIPATH_PROJECT_ID directly — no fallback chain. UiPathConfig.agent_id still falls back to project_id when UIPATH_AGENT_ID is unset (cloud-project case where the two are equal). Updated config tests; existing reporter/CLI telemetry behaviour unchanged. Refs: AE-1396 --- .../src/uipath/platform/common/_config.py | 9 ++------- .../src/uipath/platform/common/constants.py | 1 - .../tests/common/test_config_env_vars.py | 16 +++++----------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/packages/uipath-platform/src/uipath/platform/common/_config.py b/packages/uipath-platform/src/uipath/platform/common/_config.py index c7f0ed69e..cdb5eb383 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_config.py +++ b/packages/uipath-platform/src/uipath/platform/common/_config.py @@ -57,14 +57,9 @@ def config_file_name(self) -> str: @property def project_id(self) -> str | None: - from uipath.platform.common.constants import ( - ENV_UIPATH_FILE_SOURCE_PROJECT_ID, - ENV_UIPATH_PROJECT_ID, - ) + from uipath.platform.common.constants import ENV_UIPATH_PROJECT_ID - return os.getenv(ENV_UIPATH_FILE_SOURCE_PROJECT_ID) or os.getenv( - ENV_UIPATH_PROJECT_ID, None - ) + return os.getenv(ENV_UIPATH_PROJECT_ID, None) @property def agent_id(self) -> str | None: diff --git a/packages/uipath-platform/src/uipath/platform/common/constants.py b/packages/uipath-platform/src/uipath/platform/common/constants.py index cf7b2df19..304ef64a6 100644 --- a/packages/uipath-platform/src/uipath/platform/common/constants.py +++ b/packages/uipath-platform/src/uipath/platform/common/constants.py @@ -17,7 +17,6 @@ ENV_TELEMETRY_ENABLED = "UIPATH_TELEMETRY_ENABLED" ENV_TRACING_ENABLED = "UIPATH_TRACING_ENABLED" ENV_UIPATH_PROJECT_ID = "UIPATH_PROJECT_ID" -ENV_UIPATH_FILE_SOURCE_PROJECT_ID = "UIPATH_FILE_SOURCE_PROJECT_ID" ENV_UIPATH_AGENT_ID = "UIPATH_AGENT_ID" ENV_UIPATH_CLOUD_USER_ID = "UIPATH_CLOUD_USER_ID" ENV_UIPATH_PROJECT_FILES_SOURCE = "UIPATH_PROJECT_FILES_SOURCE" diff --git a/packages/uipath-platform/tests/common/test_config_env_vars.py b/packages/uipath-platform/tests/common/test_config_env_vars.py index ea301626e..8ff7b636d 100644 --- a/packages/uipath-platform/tests/common/test_config_env_vars.py +++ b/packages/uipath-platform/tests/common/test_config_env_vars.py @@ -7,7 +7,6 @@ def _clear_env(monkeypatch): for var in ( "UIPATH_PROJECT_ID", - "UIPATH_FILE_SOURCE_PROJECT_ID", "UIPATH_AGENT_ID", "UIPATH_CLOUD_USER_ID", "UIPATH_PROJECT_FILES_SOURCE", @@ -15,23 +14,18 @@ def _clear_env(monkeypatch): monkeypatch.delenv(var, raising=False) -class TestProjectIdFallback: - def test_returns_file_source_when_set(self, monkeypatch): - monkeypatch.setenv("UIPATH_FILE_SOURCE_PROJECT_ID", "file-source-id") - monkeypatch.setenv("UIPATH_PROJECT_ID", "legacy-id") +class TestProjectId: + def test_reads_env_var(self, monkeypatch): + monkeypatch.setenv("UIPATH_PROJECT_ID", "file-source-id") assert UiPathConfig.project_id == "file-source-id" - def test_falls_back_to_legacy_when_file_source_unset(self, monkeypatch): - monkeypatch.setenv("UIPATH_PROJECT_ID", "legacy-id") - assert UiPathConfig.project_id == "legacy-id" - - def test_returns_none_when_neither_set(self): + def test_returns_none_when_unset(self): assert UiPathConfig.project_id is None class TestAgentId: def test_returns_explicit_agent_id_when_set(self, monkeypatch): - monkeypatch.setenv("UIPATH_FILE_SOURCE_PROJECT_ID", "debug-project-guid") + monkeypatch.setenv("UIPATH_PROJECT_ID", "debug-project-guid") monkeypatch.setenv("UIPATH_AGENT_ID", "real-agent-id") assert UiPathConfig.agent_id == "real-agent-id"