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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath"
version = "2.10.81"
version = "2.10.82"
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"
Expand Down
13 changes: 11 additions & 2 deletions packages/uipath/src/uipath/_cli/_chat/_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,13 +518,22 @@ def get_chat_bridge(
# Build headers from context
headers = {
"Authorization": f"Bearer {os.environ.get('UIPATH_ACCESS_TOKEN', '')}",
"X-UiPath-Internal-TenantId": f"{context.tenant_id}"
"X-UiPath-Internal-TenantId": context.tenant_id
or os.environ.get("UIPATH_TENANT_ID", ""),
"X-UiPath-Internal-AccountId": f"{context.org_id}"
"X-UiPath-Internal-AccountId": context.org_id
or os.environ.get("UIPATH_ORGANIZATION_ID", ""),
"X-UiPath-ConversationId": context.conversation_id,
}
Comment on lines 525 to 526

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — fixed in 948cfa3. Dropped the f"{...}" wrapper so the or os.environ.get(...) fallback works (matches the voice bridge), and added a regression test for the env fallback when tenant_id/org_id are None.


# Conversation owner id (conversationalService.syntheticUserId) that CAS forwards via FpsProperties;
# always sent when present. It's there for RunAsMe=false, where the unattended robot's token
# subject is the robot account rather than the conversation owner, so CAS validates this presented
# id against conversation.user_id on the handshake instead of the token subject. Sent as a header
# (not a query param) to keep it out of access / load-balancer logs.
synthetic_user_id = getattr(context, "synthetic_user_id", None)
if synthetic_user_id:
headers["X-UiPath-Internal-SyntheticUserId"] = synthetic_user_id

return SocketIOChatBridge(
websocket_url=websocket_url,
websocket_path=websocket_path,
Expand Down
9 changes: 9 additions & 0 deletions packages/uipath/src/uipath/_cli/_chat/_voice_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ def get_voice_bridge(
"X-UiPath-ConversationId": context.conversation_id,
}

# Conversation owner id (conversationalService.syntheticUserId) that CAS forwards via FpsProperties;
# always sent when present. It's there for RunAsMe=false, where the unattended robot's token
# subject is the robot account rather than the conversation owner, so CAS validates this presented
# id against conversation.user_id on the handshake instead of the token subject. Sent as a header
# (not a query param) to keep it out of access / load-balancer logs.
synthetic_user_id = getattr(context, "synthetic_user_id", None)
if synthetic_user_id:
headers["X-UiPath-Internal-SyntheticUserId"] = synthetic_user_id

return VoiceToolCallSession(
url=url,
socketio_path=socketio_path,
Expand Down
47 changes: 47 additions & 0 deletions packages/uipath/tests/cli/chat/test_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,53 @@ def test_get_chat_bridge_constructs_correct_headers(
assert "X-UiPath-ConversationId" in bridge.headers
assert bridge.headers["X-UiPath-ConversationId"] == "conv-789"

def test_get_chat_bridge_falls_back_to_env_when_tenant_and_org_absent(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Tenant/account headers fall back to env vars when context values are None."""
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com/org/tenant")
monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "my-access-token")
monkeypatch.setenv("UIPATH_TENANT_ID", "env-tenant")
monkeypatch.setenv("UIPATH_ORGANIZATION_ID", "env-org")

context = MockRuntimeContext(
tenant_id=None, # type: ignore[arg-type]
org_id=None, # type: ignore[arg-type]
conversation_id="conv-789",
)

bridge = cast(SocketIOChatBridge, get_chat_bridge(cast(Any, context)))

assert bridge.headers["X-UiPath-Internal-TenantId"] == "env-tenant"
assert bridge.headers["X-UiPath-Internal-AccountId"] == "env-org"

def test_get_chat_bridge_includes_synthetic_user_id_header_when_set(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Conversation owner id (from FpsProperties) is sent on the handshake for CAS to validate."""
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com/org/tenant")
monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "my-access-token")

context = MockRuntimeContext(conversation_id="conv-789")
context.synthetic_user_id = "owner-guid" # type: ignore[attr-defined]

bridge = cast(SocketIOChatBridge, get_chat_bridge(cast(Any, context)))

assert bridge.headers["X-UiPath-Internal-SyntheticUserId"] == "owner-guid"

def test_get_chat_bridge_omits_synthetic_user_id_header_when_absent(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""No header is sent when the runtime has no owner id (backward compatible)."""
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com/org/tenant")
monkeypatch.setenv("UIPATH_ACCESS_TOKEN", "my-access-token")

context = MockRuntimeContext(conversation_id="conv-789")

bridge = cast(SocketIOChatBridge, get_chat_bridge(cast(Any, context)))

assert "X-UiPath-Internal-SyntheticUserId" not in bridge.headers

def test_get_chat_bridge_raises_without_uipath_url(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down
32 changes: 32 additions & 0 deletions packages/uipath/tests/cli/chat/test_voice_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,35 @@ def test_headers_fall_back_to_env_when_context_ids_are_none(

assert bridge._headers["X-UiPath-Internal-TenantId"] == "env-tenant"
assert bridge._headers["X-UiPath-Internal-AccountId"] == "env-org"

def test_includes_synthetic_user_id_header_when_set(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Conversation owner id (from FpsProperties) is sent on the handshake for CAS to validate."""
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com")
ctx = MagicMock(
conversation_id="conv-1",
tenant_id="t",
org_id="o",
synthetic_user_id="owner-guid",
)

bridge = get_voice_bridge(ctx, AsyncMock())

assert bridge._headers["X-UiPath-Internal-SyntheticUserId"] == "owner-guid"

def test_omits_synthetic_user_id_header_when_none(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""No header is sent when the runtime has no owner id (backward compatible)."""
monkeypatch.setenv("UIPATH_URL", "https://cloud.uipath.com")
ctx = MagicMock(
conversation_id="conv-1",
tenant_id="t",
org_id="o",
synthetic_user_id=None,
)

bridge = get_voice_bridge(ctx, AsyncMock())

assert "X-UiPath-Internal-SyntheticUserId" not in bridge._headers
2 changes: 1 addition & 1 deletion packages/uipath/uv.lock

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

Loading