Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
9 changes: 9 additions & 0 deletions packages/uipath/src/uipath/_cli/_chat/_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,15 @@ def get_chat_bridge(
"X-UiPath-ConversationId": context.conversation_id,
}
Comment thread
scottcmg marked this conversation as resolved.

# 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
27 changes: 27 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,33 @@ 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_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