From b73461c069558bdc0be67ad47a186675c567f18e Mon Sep 17 00:00:00 2001 From: Dhananjay Suresh Date: Tue, 14 Apr 2026 17:30:07 -0400 Subject: [PATCH 1/5] feat: add RunAsMe support to ProcessesService.invoke Add run_as_me parameter to ProcessesService invoke/invoke_async to support running child jobs under the calling user's identity. Also add is_conversational property to ConfigurationManager for reading runtimeOptions from uipath.json config. JAR-9434 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/uipath/platform/common/_config.py | 21 +++++ .../orchestrator/_processes_service.py | 10 ++ .../tests/services/test_processes_service.py | 93 +++++++++++++++++++ packages/uipath-platform/tests/test_config.py | 41 ++++++++ 4 files changed, 165 insertions(+) create mode 100644 packages/uipath-platform/tests/test_config.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..0145a7a2f 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_config.py +++ b/packages/uipath-platform/src/uipath/platform/common/_config.py @@ -178,11 +178,21 @@ def is_tracing_enabled(self) -> bool: return os.getenv(ENV_TRACING_ENABLED, "true").lower() == "true" + @property + def is_conversational(self) -> bool: + """Whether the current job is a conversational agent. + + Reads from runtimeOptions.isConversational in the config file. + """ + runtime_opts = self._runtime_options + return bool(runtime_opts.get("isConversational")) if runtime_opts else False + def reset(self) -> None: """Reset mutable cached state to defaults.""" self.studio_solution_id = None # Invalidate cached_property by removing from instance __dict__ self.__dict__.pop("_internal_arguments", None) + self.__dict__.pop("_runtime_options", None) def _read_internal_argument(self, key: str) -> Any: internal_args = self._internal_arguments @@ -199,5 +209,16 @@ def _internal_arguments(self) -> dict[str, Any] | None: except (FileNotFoundError, json.JSONDecodeError): return None + @cached_property + def _runtime_options(self) -> dict[str, Any] | None: + import json + + try: + with open(self.config_file_path, "r") as f: + data = json.load(f) + return data.get("runtimeOptions") + except (FileNotFoundError, json.JSONDecodeError): + return None + UiPathConfig = ConfigurationManager() diff --git a/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py b/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py index 10b6010e2..73b4122e1 100644 --- a/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py +++ b/packages/uipath-platform/src/uipath/platform/orchestrator/_processes_service.py @@ -50,6 +50,7 @@ def invoke( folder_path: Optional[str] = None, attachments: Optional[list[Attachment]] = None, parent_operation_id: Optional[str] = None, + run_as_me: Optional[bool] = None, **kwargs: Any, ) -> Job: """Start execution of a process by its name. @@ -63,6 +64,7 @@ def invoke( folder_key (Optional[str]): The key of the folder to execute the process in. Override the default one set in the SDK config. folder_path (Optional[str]): The path of the folder to execute the process in. Override the default one set in the SDK config. parent_operation_id (Optional[str]): The parent operation ID for BTS tracking correlation. + run_as_me (Optional[bool]): If True, the job will run under the calling user's identity. Returns: Job: The job execution details. @@ -100,6 +102,7 @@ def invoke( folder_path=folder_path, parent_span_id=kwargs.get("parent_span_id"), parent_operation_id=parent_operation_id, + run_as_me=run_as_me, ) response = self.request( spec.method, @@ -123,6 +126,7 @@ async def invoke_async( folder_path: Optional[str] = None, attachments: Optional[list[Attachment]] = None, parent_operation_id: Optional[str] = None, + run_as_me: Optional[bool] = None, **kwargs: Any, ) -> Job: """Asynchronously start execution of a process by its name. @@ -136,6 +140,7 @@ async def invoke_async( folder_key (Optional[str]): The key of the folder to execute the process in. Override the default one set in the SDK config. folder_path (Optional[str]): The path of the folder to execute the process in. Override the default one set in the SDK config. parent_operation_id (Optional[str]): The parent operation ID for BTS tracking correlation. + run_as_me (Optional[bool]): If True, the job will run under the calling user's identity. Returns: Job: The job execution details. @@ -168,6 +173,7 @@ async def main(): folder_path=folder_path, parent_span_id=kwargs.get("parent_span_id"), parent_operation_id=parent_operation_id, + run_as_me=run_as_me, ) response = await self.request_async( @@ -313,6 +319,7 @@ def _invoke_spec( folder_path: Optional[str] = None, parent_span_id: Optional[str] = None, parent_operation_id: Optional[str] = None, + run_as_me: Optional[bool] = None, ) -> RequestSpec: payload: Dict[str, Any] = {"ReleaseName": name, **(input_data or {})} self._add_tracing(payload, UiPathConfig.trace_id, parent_span_id) @@ -320,6 +327,9 @@ def _invoke_spec( if parent_operation_id: payload["ParentOperationId"] = parent_operation_id + if run_as_me is not None: + payload["RunAsMe"] = run_as_me + request_spec = RequestSpec( method="POST", endpoint=Endpoint( diff --git a/packages/uipath-platform/tests/services/test_processes_service.py b/packages/uipath-platform/tests/services/test_processes_service.py index 85b2e3691..15b2c9c7e 100644 --- a/packages/uipath-platform/tests/services/test_processes_service.py +++ b/packages/uipath-platform/tests/services/test_processes_service.py @@ -471,3 +471,96 @@ async def test_invoke_async_over_10k_limit_input( job_request.headers[HEADER_USER_AGENT] == f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ProcessesService.invoke_async/{version}" ) + + def test_invoke_with_run_as_me_true( + self, + httpx_mock: HTTPXMock, + service: ProcessesService, + base_url: str, + org: str, + tenant: str, + ) -> None: + process_name = "test-process" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs", + status_code=200, + json={ + "value": [ + { + "Key": "test-job-key", + "State": "Running", + "Id": 123, + "FolderKey": "test-folder-key", + } + ] + }, + ) + + service.invoke(process_name, run_as_me=True) + + sent_request = httpx_mock.get_request() + assert sent_request is not None + payload = json.loads(sent_request.content.decode("utf-8")) + assert payload["startInfo"]["RunAsMe"] is True + + def test_invoke_with_run_as_me_false( + self, + httpx_mock: HTTPXMock, + service: ProcessesService, + base_url: str, + org: str, + tenant: str, + ) -> None: + process_name = "test-process" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs", + status_code=200, + json={ + "value": [ + { + "Key": "test-job-key", + "State": "Running", + "Id": 123, + "FolderKey": "test-folder-key", + } + ] + }, + ) + + service.invoke(process_name, run_as_me=False) + + sent_request = httpx_mock.get_request() + assert sent_request is not None + payload = json.loads(sent_request.content.decode("utf-8")) + assert payload["startInfo"]["RunAsMe"] is False + + def test_invoke_without_run_as_me_excludes_from_payload( + self, + httpx_mock: HTTPXMock, + service: ProcessesService, + base_url: str, + org: str, + tenant: str, + ) -> None: + process_name = "test-process" + httpx_mock.add_response( + url=f"{base_url}{org}{tenant}/orchestrator_/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs", + status_code=200, + json={ + "value": [ + { + "Key": "test-job-key", + "State": "Running", + "Id": 123, + "FolderKey": "test-folder-key", + } + ] + }, + ) + + service.invoke(process_name) + + sent_request = httpx_mock.get_request() + assert sent_request is not None + payload = json.loads(sent_request.content.decode("utf-8")) + assert "RunAsMe" not in payload["startInfo"] diff --git a/packages/uipath-platform/tests/test_config.py b/packages/uipath-platform/tests/test_config.py new file mode 100644 index 000000000..445416070 --- /dev/null +++ b/packages/uipath-platform/tests/test_config.py @@ -0,0 +1,41 @@ +import json + +import pytest + +from uipath.platform.common._config import ConfigurationManager + + +@pytest.fixture +def config_manager(tmp_path, monkeypatch): + """Create a fresh ConfigurationManager pointing to a temp config file.""" + mgr = ConfigurationManager.__new__(ConfigurationManager) + config_file = tmp_path / "uipath.json" + monkeypatch.setenv("UIPATH_CONFIG_PATH", str(config_file)) + mgr.reset() + return mgr, config_file + + +class TestConfigurationManagerIsConversational: + def test_is_conversational_true(self, config_manager): + mgr, config_file = config_manager + config_file.write_text( + json.dumps({"runtimeOptions": {"isConversational": True}}) + ) + assert mgr.is_conversational is True + + def test_is_conversational_false(self, config_manager): + mgr, config_file = config_manager + config_file.write_text( + json.dumps({"runtimeOptions": {"isConversational": False}}) + ) + assert mgr.is_conversational is False + + def test_is_conversational_missing_runtime_options(self, config_manager): + mgr, config_file = config_manager + config_file.write_text(json.dumps({"runtime": {}})) + assert mgr.is_conversational is False + + def test_is_conversational_missing_config_file(self, config_manager): + mgr, _ = config_manager + # config_file not created, so it doesn't exist + assert mgr.is_conversational is False From c527a87a148ce260bb04ed19d117a7495e33ba18 Mon Sep 17 00:00:00 2001 From: Dhananjay Suresh Date: Wed, 15 Apr 2026 15:32:01 -0400 Subject: [PATCH 2/5] chore: bump uipath-platform to 0.1.29 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/uipath-platform/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 61d507e60..1b2b9a9aa 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.1.28" +version = "0.1.29" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" From bd3c188761def153a891f3725623e93f0bf52e0a Mon Sep 17 00:00:00 2001 From: Dhananjay Suresh Date: Wed, 15 Apr 2026 15:43:42 -0400 Subject: [PATCH 3/5] chore: update lockfiles for uipath-platform 0.1.29 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/uipath-platform/uv.lock | 2 +- packages/uipath/uv.lock | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index dce3eb8e9..e9a3d8d75 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.28" +version = "0.1.29" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 6536f439a..fbb233ba2 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2616,7 +2616,7 @@ requires-dist = [ { name = "truststore", specifier = ">=0.10.1" }, { name = "uipath-core", editable = "../uipath-core" }, { name = "uipath-platform", editable = "../uipath-platform" }, - { name = "uipath-runtime", specifier = ">=0.10.0,<0.11.0" }, + { name = "uipath-runtime", editable = "../../../uipath-runtime-python" }, ] [package.metadata.requires-dev] @@ -2682,7 +2682,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.1.28" +version = "0.1.29" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" }, @@ -2721,13 +2721,27 @@ dev = [ [[package]] name = "uipath-runtime" version = "0.10.0" -source = { registry = "https://pypi.org/simple" } +source = { editable = "../../../uipath-runtime-python" } dependencies = [ { name = "uipath-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/64/69462ee01a5607ce36b1fa152c52ac72fb28abe0aa049394406fc0b31525/uipath_runtime-0.10.0.tar.gz", hash = "sha256:d27d58e2252f506c8c0e00f814b37c3863150e8ffcde8e4c6ab14bd98febd3df", size = 139626, upload-time = "2026-03-24T19:42:43.738Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/ed/9c0e97a078b96e4d3742ea3515cb30886b08579cd08077cd42a159adf70d/uipath_runtime-0.10.0-py3-none-any.whl", hash = "sha256:4f52df0b56f54e70fcf34fbf74e223d02b97b5a6fd6d8f64bc06782bb5484b07", size = 42097, upload-time = "2026-03-24T19:42:42.359Z" }, + +[package.metadata] +requires-dist = [{ name = "uipath-core", editable = "../uipath-core" }] + +[package.metadata.requires-dev] +dev = [ + { name = "bandit", specifier = ">=1.8.2" }, + { name = "mypy", specifier = ">=1.14.1" }, + { name = "pre-commit", specifier = ">=4.1.0" }, + { name = "pytest", specifier = ">=7.4.0" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "pytest-cov", specifier = ">=4.1.0" }, + { name = "pytest-httpx", specifier = ">=0.35.0" }, + { name = "pytest-mock", specifier = ">=3.11.1" }, + { name = "pytest-trio", specifier = ">=0.8.0" }, + { name = "ruff", specifier = ">=0.9.4" }, + { name = "rust-just", specifier = ">=1.39.0" }, ] [[package]] From 790d36411de381ec7b0a6075f134054e0014befa Mon Sep 17 00:00:00 2001 From: Dhananjay Suresh Date: Thu, 16 Apr 2026 10:43:54 -0400 Subject: [PATCH 4/5] refactor: remove is_conversational from ConfigurationManager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per PR review feedback — is_conversational should come from the agent definition top-down, not from ConfigurationManager. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/uipath/platform/common/_config.py | 21 ---------- packages/uipath-platform/tests/test_config.py | 41 ------------------- 2 files changed, 62 deletions(-) delete mode 100644 packages/uipath-platform/tests/test_config.py diff --git a/packages/uipath-platform/src/uipath/platform/common/_config.py b/packages/uipath-platform/src/uipath/platform/common/_config.py index 0145a7a2f..b656830f6 100644 --- a/packages/uipath-platform/src/uipath/platform/common/_config.py +++ b/packages/uipath-platform/src/uipath/platform/common/_config.py @@ -178,21 +178,11 @@ def is_tracing_enabled(self) -> bool: return os.getenv(ENV_TRACING_ENABLED, "true").lower() == "true" - @property - def is_conversational(self) -> bool: - """Whether the current job is a conversational agent. - - Reads from runtimeOptions.isConversational in the config file. - """ - runtime_opts = self._runtime_options - return bool(runtime_opts.get("isConversational")) if runtime_opts else False - def reset(self) -> None: """Reset mutable cached state to defaults.""" self.studio_solution_id = None # Invalidate cached_property by removing from instance __dict__ self.__dict__.pop("_internal_arguments", None) - self.__dict__.pop("_runtime_options", None) def _read_internal_argument(self, key: str) -> Any: internal_args = self._internal_arguments @@ -209,16 +199,5 @@ def _internal_arguments(self) -> dict[str, Any] | None: except (FileNotFoundError, json.JSONDecodeError): return None - @cached_property - def _runtime_options(self) -> dict[str, Any] | None: - import json - - try: - with open(self.config_file_path, "r") as f: - data = json.load(f) - return data.get("runtimeOptions") - except (FileNotFoundError, json.JSONDecodeError): - return None - UiPathConfig = ConfigurationManager() diff --git a/packages/uipath-platform/tests/test_config.py b/packages/uipath-platform/tests/test_config.py deleted file mode 100644 index 445416070..000000000 --- a/packages/uipath-platform/tests/test_config.py +++ /dev/null @@ -1,41 +0,0 @@ -import json - -import pytest - -from uipath.platform.common._config import ConfigurationManager - - -@pytest.fixture -def config_manager(tmp_path, monkeypatch): - """Create a fresh ConfigurationManager pointing to a temp config file.""" - mgr = ConfigurationManager.__new__(ConfigurationManager) - config_file = tmp_path / "uipath.json" - monkeypatch.setenv("UIPATH_CONFIG_PATH", str(config_file)) - mgr.reset() - return mgr, config_file - - -class TestConfigurationManagerIsConversational: - def test_is_conversational_true(self, config_manager): - mgr, config_file = config_manager - config_file.write_text( - json.dumps({"runtimeOptions": {"isConversational": True}}) - ) - assert mgr.is_conversational is True - - def test_is_conversational_false(self, config_manager): - mgr, config_file = config_manager - config_file.write_text( - json.dumps({"runtimeOptions": {"isConversational": False}}) - ) - assert mgr.is_conversational is False - - def test_is_conversational_missing_runtime_options(self, config_manager): - mgr, config_file = config_manager - config_file.write_text(json.dumps({"runtime": {}})) - assert mgr.is_conversational is False - - def test_is_conversational_missing_config_file(self, config_manager): - mgr, _ = config_manager - # config_file not created, so it doesn't exist - assert mgr.is_conversational is False From 2ac09a346c6fa32bbf296043b36183d3fb8f0d77 Mon Sep 17 00:00:00 2001 From: Dhananjay Suresh Date: Thu, 16 Apr 2026 12:22:52 -0400 Subject: [PATCH 5/5] chore: regenerate uv.lock after rebase Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/uipath/uv.lock | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index fbb233ba2..f61946b1a 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2616,7 +2616,7 @@ requires-dist = [ { name = "truststore", specifier = ">=0.10.1" }, { name = "uipath-core", editable = "../uipath-core" }, { name = "uipath-platform", editable = "../uipath-platform" }, - { name = "uipath-runtime", editable = "../../../uipath-runtime-python" }, + { name = "uipath-runtime", specifier = ">=0.10.0,<0.11.0" }, ] [package.metadata.requires-dev] @@ -2721,27 +2721,13 @@ dev = [ [[package]] name = "uipath-runtime" version = "0.10.0" -source = { editable = "../../../uipath-runtime-python" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "uipath-core" }, ] - -[package.metadata] -requires-dist = [{ name = "uipath-core", editable = "../uipath-core" }] - -[package.metadata.requires-dev] -dev = [ - { name = "bandit", specifier = ">=1.8.2" }, - { name = "mypy", specifier = ">=1.14.1" }, - { name = "pre-commit", specifier = ">=4.1.0" }, - { name = "pytest", specifier = ">=7.4.0" }, - { name = "pytest-asyncio", specifier = ">=1.0.0" }, - { name = "pytest-cov", specifier = ">=4.1.0" }, - { name = "pytest-httpx", specifier = ">=0.35.0" }, - { name = "pytest-mock", specifier = ">=3.11.1" }, - { name = "pytest-trio", specifier = ">=0.8.0" }, - { name = "ruff", specifier = ">=0.9.4" }, - { name = "rust-just", specifier = ">=1.39.0" }, +sdist = { url = "https://files.pythonhosted.org/packages/75/64/69462ee01a5607ce36b1fa152c52ac72fb28abe0aa049394406fc0b31525/uipath_runtime-0.10.0.tar.gz", hash = "sha256:d27d58e2252f506c8c0e00f814b37c3863150e8ffcde8e4c6ab14bd98febd3df", size = 139626, upload-time = "2026-03-24T19:42:43.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/ed/9c0e97a078b96e4d3742ea3515cb30886b08579cd08077cd42a159adf70d/uipath_runtime-0.10.0-py3-none-any.whl", hash = "sha256:4f52df0b56f54e70fcf34fbf74e223d02b97b5a6fd6d8f64bc06782bb5484b07", size = 42097, upload-time = "2026-03-24T19:42:42.359Z" }, ] [[package]]