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-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-platform"
version = "0.1.73"
version = "0.1.74"
description = "HTTP client library for programmatic access to UiPath Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
10 changes: 10 additions & 0 deletions packages/uipath-platform/src/uipath/platform/common/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,16 @@ def job_key(self) -> str | None:

return os.getenv(ENV_JOB_KEY, None)

@property
def execution_source(self) -> str | None:
"""Execution source identifying the run context.

Set by the ``uipath`` CLI from the executing command (run/debug/eval).
"""
from uipath.platform.common.constants import ENV_EXECUTION_SOURCE

return os.getenv(ENV_EXECUTION_SOURCE, None)

@property
def has_legacy_eval_folder(self) -> bool:
from uipath.platform.common.constants import LEGACY_EVAL_FOLDER
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ._config import UiPathConfig
from .constants import HEADER_JOB_KEY
from .constants import HEADER_GUARDRAILS_SOURCE, HEADER_JOB_KEY


def header_job_key() -> dict[str, str]:
Expand All @@ -11,3 +11,15 @@ def header_job_key() -> dict[str, str]:
if not job_key:
return {}
return {HEADER_JOB_KEY: job_key}


def header_execution_source() -> dict[str, str]:
"""Return the x-uipath-guardrails-source header for the execution source.
Returns an empty dict when ``UiPathConfig.execution_source`` is unset or
empty.
"""
source = UiPathConfig.execution_source
if not source:
return {}
return {HEADER_GUARDRAILS_SOURCE: source}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
ENV_UIPATH_TRACE_ID = "UIPATH_TRACE_ID"
ENV_UIPATH_PROCESS_VERSION = "UIPATH_PROCESS_VERSION"
ENV_UIPATH_CONFIG_PATH = "UIPATH_CONFIG_PATH"
ENV_EXECUTION_SOURCE = "UIPATH_EXECUTION_SOURCE"

# Headers
HEADER_FOLDER_KEY = "x-uipath-folderkey"
Expand All @@ -39,6 +40,7 @@
HEADER_PROCESS_KEY = "x-uipath-processkey"
HEADER_TRACE_ID = "x-uipath-traceid"
HEADER_AGENTHUB_CONFIG = "x-uipath-agenthub-config"
HEADER_GUARDRAILS_SOURCE = "x-uipath-guardrails-source"
HEADER_LLMGATEWAY_BYO_CONNECTION_ID = "x-uipath-llmgateway-byoisconnectionid"
HEADER_SW_LOCK_KEY = "x-uipath-sw-lockkey"
HEADER_LICENSING_CONTEXT = "x-uipath-licensing-context"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ..common._base_service import BaseService
from ..common._config import UiPathApiConfig
from ..common._execution_context import UiPathExecutionContext
from ..common._job_context import header_execution_source, header_job_key
from ..common._models import Endpoint, RequestSpec
from ..errors import EnrichedException
from .guardrails import BuiltInValidatorGuardrail
Expand Down Expand Up @@ -122,9 +123,16 @@ def evaluate_guardrail(
endpoint=Endpoint("/agentsruntime_/api/execution/guardrails/validate"),
json=payload,
)
# Include trace context headers for server-side span correlation
# Include trace context headers for server-side span correlation, plus
# the execution source (x-uipath-guardrails-source) and job key headers
# for licensing/metering correlation.
trace_headers = build_trace_context_headers()
request_headers = {**(spec.headers or {}), **trace_headers}
request_headers = {
**(spec.headers or {}),
**trace_headers,
**header_execution_source(),
**header_job_key(),
}
span_id = None
try:
response = self.request(
Expand Down
30 changes: 28 additions & 2 deletions packages/uipath-platform/tests/common/test_job_context.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import pytest

from uipath.platform.common._job_context import header_job_key
from uipath.platform.common.constants import ENV_JOB_KEY, HEADER_JOB_KEY
from uipath.platform.common._job_context import (
header_execution_source,
header_job_key,
)
from uipath.platform.common.constants import (
ENV_EXECUTION_SOURCE,
ENV_JOB_KEY,
HEADER_GUARDRAILS_SOURCE,
HEADER_JOB_KEY,
)


def test_returns_header_when_env_var_set(monkeypatch: pytest.MonkeyPatch) -> None:
Expand All @@ -20,3 +28,21 @@ def test_returns_empty_when_env_var_blank(monkeypatch: pytest.MonkeyPatch) -> No
monkeypatch.setenv(ENV_JOB_KEY, "")

assert header_job_key() == {}


def test_execution_source_header_when_set(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(ENV_EXECUTION_SOURCE, "runtime")

assert header_execution_source() == {HEADER_GUARDRAILS_SOURCE: "runtime"}


def test_execution_source_empty_when_unset(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv(ENV_EXECUTION_SOURCE, raising=False)

assert header_execution_source() == {}


def test_execution_source_empty_when_blank(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(ENV_EXECUTION_SOURCE, "")

assert header_execution_source() == {}
98 changes: 98 additions & 0 deletions packages/uipath-platform/tests/services/test_guardrails_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,104 @@ def capture_request(request):
# header merging works even when no active span exists)
assert "content-type" in headers

def test_evaluate_guardrail_sends_source_and_job_key_headers(
self,
httpx_mock: HTTPXMock,
service: GuardrailsService,
base_url: str,
org: str,
tenant: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Outgoing request includes execution source and job key headers."""
monkeypatch.setenv("UIPATH_EXECUTION_SOURCE", "runtime")
monkeypatch.setenv("UIPATH_JOB_KEY", "job-123")

captured_request = None

def capture_request(request):
nonlocal captured_request
captured_request = request
return httpx.Response(
status_code=200,
json={"result": "PASSED", "details": "OK"},
)

httpx_mock.add_callback(
method="POST",
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
callback=capture_request,
)

pii_guardrail = BuiltInValidatorGuardrail(
id="test-id",
name="PII guardrail",
description="Test",
enabled_for_evals=True,
selector=GuardrailSelector(
scopes=[GuardrailScope.TOOL], match_names=["tool1"]
),
guardrail_type="builtInValidator",
validator_type="pii_detection",
validator_parameters=[],
)

service.evaluate_guardrail("test input", pii_guardrail)

assert captured_request is not None
headers = dict(captured_request.headers)
assert headers.get("x-uipath-guardrails-source") == "runtime"
assert headers.get("x-uipath-jobkey") == "job-123"

def test_evaluate_guardrail_omits_source_and_job_key_when_unset(
self,
httpx_mock: HTTPXMock,
service: GuardrailsService,
base_url: str,
org: str,
tenant: str,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Source/job key headers are absent when their env vars are unset."""
monkeypatch.delenv("UIPATH_EXECUTION_SOURCE", raising=False)
monkeypatch.delenv("UIPATH_JOB_KEY", raising=False)

captured_request = None

def capture_request(request):
nonlocal captured_request
captured_request = request
return httpx.Response(
status_code=200,
json={"result": "PASSED", "details": "OK"},
)

httpx_mock.add_callback(
method="POST",
url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate",
callback=capture_request,
)

pii_guardrail = BuiltInValidatorGuardrail(
id="test-id",
name="PII guardrail",
description="Test",
enabled_for_evals=True,
selector=GuardrailSelector(
scopes=[GuardrailScope.TOOL], match_names=["tool1"]
),
guardrail_type="builtInValidator",
validator_type="pii_detection",
validator_parameters=[],
)

service.evaluate_guardrail("test input", pii_guardrail)

assert captured_request is not None
headers = dict(captured_request.headers)
assert "x-uipath-guardrails-source" not in headers
assert "x-uipath-jobkey" not in headers

def test_evaluate_guardrail_extracts_span_id_from_traceparent(
self,
httpx_mock: HTTPXMock,
Expand Down
2 changes: 1 addition & 1 deletion packages/uipath-platform/uv.lock

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

4 changes: 2 additions & 2 deletions packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "uipath"
version = "2.11.9"
version = "2.11.10"
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"
dependencies = [
"uipath-core>=0.5.21, <0.6.0",
"uipath-runtime>=0.11.0, <0.12.0",
"uipath-platform>=0.1.73, <0.2.0",
"uipath-platform>=0.1.74, <0.2.0",
"click>=8.3.1",
"httpx>=0.28.1",
"pyjwt>=2.10.1",
Expand Down
40 changes: 40 additions & 0 deletions packages/uipath/src/uipath/_cli/_utils/_execution_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Execution source resolution for the CLI.

Maps the executing CLI command to an AgentHub config ("source") and exposes it
to the SDK via the ``UIPATH_EXECUTION_SOURCE`` environment variable. Because
this runs in the same process that later executes the agent, downstream
services (e.g. guardrails) can read it from config and send it as the
``x-uipath-agenthub-config`` header. This covers both coded and low-code
agents, since both are launched through these CLI commands.
"""

import os

from uipath.platform.common.constants import ENV_EXECUTION_SOURCE

# Execution source values, matching the low-code runtime's AgentExecutionType
# (run -> runtime, debug/dev -> playground, eval -> eval).
_RUNTIME_SOURCE = "runtime"
_PLAYGROUND_SOURCE = "playground"
_EVAL_SOURCE = "eval"

_COMMAND_TO_SOURCE: dict[str, str] = {
"run": _RUNTIME_SOURCE,
"debug": _PLAYGROUND_SOURCE,
"dev": _PLAYGROUND_SOURCE,
"eval": _EVAL_SOURCE,
}


def set_execution_source(command: str) -> None:
"""Set ``UIPATH_EXECUTION_SOURCE`` from the executing CLI command.

No-op for commands that do not execute an agent.

Args:
command: The CLI command name (e.g. "run", "debug", "eval").
"""
source = _COMMAND_TO_SOURCE.get(command)
if source is None:
return
os.environ[ENV_EXECUTION_SOURCE] = source
4 changes: 4 additions & 0 deletions packages/uipath/src/uipath/_cli/cli_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ def debug(
attach: str | None,
) -> None:
"""Debug the project."""
from ._utils._execution_source import set_execution_source

set_execution_source("debug")

input_file = file or input_file
# Setup debugging if requested
if not setup_debugging(debug, debug_port):
Expand Down
4 changes: 4 additions & 0 deletions packages/uipath/src/uipath/_cli/cli_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ def dev(interface: str, debug: bool, debug_port: int) -> None:

INTERFACE: Choose 'terminal' for console interface (default) or 'web' for browser-based interface.
"""
from ._utils._execution_source import set_execution_source

set_execution_source("dev")

try:
_check_dev_dependency(interface)
except ImportError as e:
Expand Down
4 changes: 4 additions & 0 deletions packages/uipath/src/uipath/_cli/cli_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ def eval(
input_overrides: Input field overrides mapping (direct field override with deep merge)
resume: Resume execution from a previous suspended state
"""
from ._utils._execution_source import set_execution_source

set_execution_source("eval")

set_llm_concurrency(max_llm_concurrency)

should_register_progress_reporter = setup_reporting_prereq(no_report)
Expand Down
4 changes: 4 additions & 0 deletions packages/uipath/src/uipath/_cli/cli_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ def run(
simulation: str | None,
) -> None:
"""Execute the project."""
from ._utils._execution_source import set_execution_source

set_execution_source("run")

input_file = file or input_file

# Setup debugging if requested
Expand Down
34 changes: 34 additions & 0 deletions packages/uipath/tests/cli/unit/test_execution_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os

import pytest

from uipath._cli._utils._execution_source import set_execution_source


@pytest.mark.parametrize(
"command,expected",
[
("run", "runtime"),
("debug", "playground"),
("dev", "playground"),
("eval", "eval"),
],
)
def test_set_execution_source_maps_command(
command: str, expected: str, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv("UIPATH_EXECUTION_SOURCE", raising=False)

set_execution_source(command)

assert os.environ["UIPATH_EXECUTION_SOURCE"] == expected


def test_set_execution_source_noop_for_unmapped_command(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("UIPATH_EXECUTION_SOURCE", raising=False)

set_execution_source("pack")

assert "UIPATH_EXECUTION_SOURCE" not in os.environ
4 changes: 2 additions & 2 deletions packages/uipath/uv.lock

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

Loading