From a7485fb273ef606ee3e2d6507e80da7535e45f76 Mon Sep 17 00:00:00 2001 From: Cosmin Maria Date: Tue, 9 Jun 2026 16:48:44 +0300 Subject: [PATCH 1/2] fix: bump uipath-langchain-client to 1.13.1 for rt_ token support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the `uipath-langchain-client` dependency (and the transitive `uipath-llm-client`) from 1.13.0 to 1.13.1 across all extras. 1.13.1 fixes authentication with opaque UiPath reference tokens (prefixed `rt_`). Previously `PlatformSettings` validation decoded every access token as a JWT to check expiry and extract the `client_id`, which raised `ValueError` for non-JWT `rt_` tokens — so agents authenticating with a reference token could not construct a chat model. 1.13.1 parses JWT claims best-effort and falls back gracefully for opaque tokens. Add `tests/chat/test_rt_token_auth.py` pinning this behaviour: an `rt_` reference token passes validation and is sent verbatim as the Bearer credential, while JWT parsing (claim extraction and expiry rejection) stays unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- pyproject.toml | 14 ++--- tests/chat/test_rt_token_auth.py | 88 ++++++++++++++++++++++++++++++++ uv.lock | 26 +++++----- 3 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 tests/chat/test_rt_token_auth.py diff --git a/pyproject.toml b/pyproject.toml index a0053aa9c..f8bf7d37a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "langchain-mcp-adapters==0.2.1", "pillow>=12.1.1", "a2a-sdk>=0.2.0,<1.0.0", - "uipath-langchain-client[openai]>=1.13.0,<1.14.0", + "uipath-langchain-client[openai]>=1.13.1,<1.14.0", ] classifiers = [ @@ -40,21 +40,21 @@ maintainers = [ [project.optional-dependencies] anthropic = [ - "uipath-langchain-client[anthropic]>=1.13.0,<1.14.0", + "uipath-langchain-client[anthropic]>=1.13.1,<1.14.0", ] vertex = [ - "uipath-langchain-client[google]>=1.13.0,<1.14.0", - "uipath-langchain-client[vertexai]>=1.13.0,<1.14.0", + "uipath-langchain-client[google]>=1.13.1,<1.14.0", + "uipath-langchain-client[vertexai]>=1.13.1,<1.14.0", ] bedrock = [ - "uipath-langchain-client[bedrock]>=1.13.0,<1.14.0", + "uipath-langchain-client[bedrock]>=1.13.1,<1.14.0", "boto3-stubs>=1.41.4", ] fireworks = [ - "uipath-langchain-client[fireworks]>=1.13.0,<1.14.0", + "uipath-langchain-client[fireworks]>=1.13.1,<1.14.0", ] all = [ - "uipath-langchain-client[all]>=1.13.0,<1.14.0", + "uipath-langchain-client[all]>=1.13.1,<1.14.0", ] [project.entry-points."uipath.middlewares"] diff --git a/tests/chat/test_rt_token_auth.py b/tests/chat/test_rt_token_auth.py new file mode 100644 index 000000000..97e1b7ba6 --- /dev/null +++ b/tests/chat/test_rt_token_auth.py @@ -0,0 +1,88 @@ +"""Reference-token (``rt_``) support in ``PlatformSettings``. + +UiPath issues two kinds of access tokens: JWT bearer tokens, and opaque +*reference tokens* (prefixed ``rt_``) that carry no client-readable claims. + +Before ``uipath-langchain-client`` 1.13.1 the platform settings validator +decoded *every* token as a JWT (splitting on ``.`` and base64-decoding the +payload) to check expiry and pull out the ``client_id``. An opaque ``rt_`` +token has no dot-separated payload, so that decode raised ``ValueError`` and +an agent authenticating with a reference token could never construct a chat +model. 1.13.1 parses JWT claims best-effort and falls back gracefully for +opaque tokens. These tests pin that behaviour so the dependency cannot +regress underneath us. +""" + +import base64 +import json +import time + +import httpx +import pytest +from pydantic import SecretStr +from uipath_langchain_client.settings import PlatformSettings + +# An opaque UiPath reference token: no ``.``-separated JWT payload to decode. +_RT_TOKEN = "rt_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +_BASE_URL = "https://alpha.uipath.com/TestOrg/TestTenant" + + +def _make_jwt(payload: dict) -> str: + """Build a JWT-shaped token (``header.payload.signature``) for ``payload``.""" + + def _segment(obj: dict) -> str: + raw = json.dumps(obj).encode() + return base64.urlsafe_b64encode(raw).rstrip(b"=").decode() + + return f"{_segment({'alg': 'none'})}.{_segment(payload)}.signature" + + +def _make_settings(token: str) -> PlatformSettings: + return PlatformSettings( + base_url=_BASE_URL, + organization_id="TestOrg", + tenant_id="TestTenant", + access_token=SecretStr(token), + ) + + +class TestReferenceTokenSettings: + """``PlatformSettings`` must accept opaque ``rt_`` reference tokens.""" + + def test_reference_token_does_not_raise(self) -> None: + """An opaque ``rt_`` token passes validation (the 1.13.1 fix).""" + settings = _make_settings(_RT_TOKEN) + + assert settings.access_token.get_secret_value() == _RT_TOKEN + # Opaque tokens carry no JWT claims, so no client_id is extracted. + assert settings.client_id is None + + def test_reference_token_used_as_bearer(self) -> None: + """The reference token is sent verbatim as the Bearer credential.""" + settings = _make_settings(_RT_TOKEN) + auth = settings.build_auth_pipeline() + + request = httpx.Request("POST", f"{_BASE_URL}/llm") + authenticated = next(auth.auth_flow(request)) + + assert authenticated.headers["Authorization"] == f"Bearer {_RT_TOKEN}" + + +class TestJwtTokenStillSupported: + """Regression guard: JWT handling must be unchanged by the ``rt_`` fix.""" + + def test_jwt_claims_extracted(self) -> None: + """A JWT still has its ``client_id`` claim extracted.""" + token = _make_jwt({"client_id": "the-client-id", "exp": time.time() + 3600}) + + settings = _make_settings(token) + + assert settings.client_id == "the-client-id" + + def test_expired_jwt_rejected(self) -> None: + """An expired JWT is still rejected during validation.""" + token = _make_jwt({"exp": time.time() - 3600}) + + with pytest.raises(ValueError, match="expired"): + _make_settings(token) diff --git a/uv.lock b/uv.lock index a425af33d..1f91abdae 100644 --- a/uv.lock +++ b/uv.lock @@ -4465,13 +4465,13 @@ requires-dist = [ { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "uipath", specifier = ">=2.10.79,<2.11.0" }, { name = "uipath-core", specifier = ">=0.5.17,<0.6.0" }, - { name = "uipath-langchain-client", extras = ["all"], marker = "extra == 'all'", specifier = ">=1.13.0,<1.14.0" }, - { name = "uipath-langchain-client", extras = ["anthropic"], marker = "extra == 'anthropic'", specifier = ">=1.13.0,<1.14.0" }, - { name = "uipath-langchain-client", extras = ["bedrock"], marker = "extra == 'bedrock'", specifier = ">=1.13.0,<1.14.0" }, - { name = "uipath-langchain-client", extras = ["fireworks"], marker = "extra == 'fireworks'", specifier = ">=1.13.0,<1.14.0" }, - { name = "uipath-langchain-client", extras = ["google"], marker = "extra == 'vertex'", specifier = ">=1.13.0,<1.14.0" }, - { name = "uipath-langchain-client", extras = ["openai"], specifier = ">=1.13.0,<1.14.0" }, - { name = "uipath-langchain-client", extras = ["vertexai"], marker = "extra == 'vertex'", specifier = ">=1.13.0,<1.14.0" }, + { name = "uipath-langchain-client", extras = ["all"], marker = "extra == 'all'", specifier = ">=1.13.1,<1.14.0" }, + { name = "uipath-langchain-client", extras = ["anthropic"], marker = "extra == 'anthropic'", specifier = ">=1.13.1,<1.14.0" }, + { name = "uipath-langchain-client", extras = ["bedrock"], marker = "extra == 'bedrock'", specifier = ">=1.13.1,<1.14.0" }, + { name = "uipath-langchain-client", extras = ["fireworks"], marker = "extra == 'fireworks'", specifier = ">=1.13.1,<1.14.0" }, + { name = "uipath-langchain-client", extras = ["google"], marker = "extra == 'vertex'", specifier = ">=1.13.1,<1.14.0" }, + { name = "uipath-langchain-client", extras = ["openai"], specifier = ">=1.13.1,<1.14.0" }, + { name = "uipath-langchain-client", extras = ["vertexai"], marker = "extra == 'vertex'", specifier = ">=1.13.1,<1.14.0" }, { name = "uipath-platform", specifier = ">=0.1.61,<0.2.0" }, { name = "uipath-runtime", specifier = ">=0.11.0,<0.12.0" }, ] @@ -4495,15 +4495,15 @@ dev = [ [[package]] name = "uipath-langchain-client" -version = "1.13.0" +version = "1.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain" }, { name = "uipath-llm-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/b4/66fa04f9c31e8a3849881bb57b00bfedafcb7637bdd84d2ee216e6c59af2/uipath_langchain_client-1.13.0.tar.gz", hash = "sha256:be81eb2054d610a27275a66f9488a782b0be6249a31441e08a874c1337f18290", size = 37242, upload-time = "2026-05-28T10:12:24.876Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/42/db3235d736d64da37d91a66f830645daa26c80c07f59bd2fd4c38098692c/uipath_langchain_client-1.13.1.tar.gz", hash = "sha256:52e7593dd8442aa7cad9f6295bcc36f48d19600a54466bda07c5428cf3c8122a", size = 37377, upload-time = "2026-06-09T13:21:42.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/2e/dc9f0bfe47d5d8d4d407b2a7c14b87c8c6d3f3771a2b1c5d51ed58b327f5/uipath_langchain_client-1.13.0-py3-none-any.whl", hash = "sha256:095b14e58ce3ee9a3dee6ecf2f6c70a46bceef93aeed829c7fe62d14610f08ef", size = 46402, upload-time = "2026-05-28T10:12:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3e/f8041aaf6eb37d37b91a3a685d8e26d18b2383b1965bfa9cb3a514576739/uipath_langchain_client-1.13.1-py3-none-any.whl", hash = "sha256:c19723f8715664f1344ade4b5344344b62a576de764c57e7fa295e11976b9599", size = 46403, upload-time = "2026-06-09T13:21:41.061Z" }, ] [package.optional-dependencies] @@ -4540,7 +4540,7 @@ vertexai = [ [[package]] name = "uipath-llm-client" -version = "1.13.0" +version = "1.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -4549,9 +4549,9 @@ dependencies = [ { name = "tenacity" }, { name = "uipath-platform" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/f5/b4ee28bbe73e1916cada12b6e7493b7896bda74bfb202a1eaddfd890f538/uipath_llm_client-1.13.0.tar.gz", hash = "sha256:f4e78a1c14317bd4e844af37ec27e8b6b65901217157b4c7dc41bd1de014f001", size = 12496696, upload-time = "2026-05-28T10:11:04.383Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/17/69a5547e470370ac3299f228b57b7458e20301e05b9b32d78f685f72ff80/uipath_llm_client-1.13.1.tar.gz", hash = "sha256:428b40e36315c626adef0a6f5c3f9f5838825503966074f8d430dfe8e84a6ad5", size = 12498258, upload-time = "2026-06-09T13:18:14.654Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/76/216eac61159731af1baf6e497511b4c30280362957dc574240d021783357/uipath_llm_client-1.13.0-py3-none-any.whl", hash = "sha256:866c6a1f590c45f8caa0925cc3c02e8437e7f640cd2b657161d0699be60342ef", size = 65698, upload-time = "2026-05-28T10:11:02.363Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/899697da1e57cc061455bed944f90ce3bf1c4cc32173832b13428f35ca0b/uipath_llm_client-1.13.1-py3-none-any.whl", hash = "sha256:7a2e0e0a004b65c82b2161739399107335e87d4f2f2c43e8edacc4d4296ee7e9", size = 66056, upload-time = "2026-06-09T13:18:13.073Z" }, ] [[package]] From 46509b1d1129458807292fade6dddcdb72a01c21 Mon Sep 17 00:00:00 2001 From: Cosmin Maria Date: Tue, 9 Jun 2026 17:02:29 +0300 Subject: [PATCH 2/2] fix: add type arguments to dict hints in rt_ token test mypy runs with disallow_any_generics, so the bare `dict` annotations in the JWT helper need explicit type arguments. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/chat/test_rt_token_auth.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/chat/test_rt_token_auth.py b/tests/chat/test_rt_token_auth.py index 97e1b7ba6..c8a25db87 100644 --- a/tests/chat/test_rt_token_auth.py +++ b/tests/chat/test_rt_token_auth.py @@ -16,6 +16,7 @@ import base64 import json import time +from typing import Any import httpx import pytest @@ -28,10 +29,10 @@ _BASE_URL = "https://alpha.uipath.com/TestOrg/TestTenant" -def _make_jwt(payload: dict) -> str: +def _make_jwt(payload: dict[str, Any]) -> str: """Build a JWT-shaped token (``header.payload.signature``) for ``payload``.""" - def _segment(obj: dict) -> str: + def _segment(obj: dict[str, Any]) -> str: raw = json.dumps(obj).encode() return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()