From abd5a4b815902b02abdf678e4c77cdd98748a678 Mon Sep 17 00:00:00 2001 From: Valentina Bojan Date: Mon, 13 Jul 2026 18:44:46 +0300 Subject: [PATCH] feat(guardrails): support BYOG evaluation via byoValidatorName Bring-Your-Own-Guardrail (BYOG) guardrails are represented as a BuiltInValidatorGuardrail with the "byo" sentinel validator_type and a byoValidatorName referencing the connector-backed configuration. Add the typed field and forward byoValidatorName in the evaluation request so the guardrails service can resolve and dispatch to the connector, and fail fast when a "byo" guardrail is missing the reference. Built-in validators are unchanged. Bumps uipath-platform to 0.2.9. Co-Authored-By: Claude Opus 4.8 (1M context) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/uipath-platform/pyproject.toml | 2 +- .../guardrails/_guardrails_service.py | 10 +- .../uipath/platform/guardrails/guardrails.py | 6 + .../tests/services/test_guardrails_service.py | 167 ++++++++++++++++++ packages/uipath-platform/uv.lock | 4 +- packages/uipath/uv.lock | 4 +- 6 files changed, 186 insertions(+), 7 deletions(-) diff --git a/packages/uipath-platform/pyproject.toml b/packages/uipath-platform/pyproject.toml index 01353417d..42178758a 100644 --- a/packages/uipath-platform/pyproject.toml +++ b/packages/uipath-platform/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-platform" -version = "0.2.8" +version = "0.2.9" description = "HTTP client library for programmatic access to UiPath Platform" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py index f4c7d832e..b73d810e7 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py @@ -17,7 +17,7 @@ from ..common._job_context import header_job_key from ..common._models import Endpoint, RequestSpec from ..errors import EnrichedException -from .guardrails import BuiltInValidatorGuardrail +from .guardrails import BYO_VALIDATOR_TYPE, BuiltInValidatorGuardrail # x-uipath-traceparent-id header format: {version}-{trace_id}-{span_id}[-{trace_flags}] # Based on W3C traceparent but allows 16- or 32-hex span IDs. @@ -115,12 +115,18 @@ def evaluate_guardrail( parameters = [ param.model_dump(by_alias=True) for param in guardrail.validator_parameters ] - payload = { + payload: dict[str, Any] = { "validator": guardrail.validator_type, "input": input_data if isinstance(input_data, str) else str(input_data), "parameters": parameters, "guardrailName": guardrail.name, } + if guardrail.validator_type == BYO_VALIDATOR_TYPE: + if not guardrail.byo_validator_name: + raise ValueError( + "BYO (Bring Your Own) guardrails require byo_validator_name." + ) + payload["byoValidatorName"] = guardrail.byo_validator_name spec = RequestSpec( method="POST", endpoint=Endpoint("/agentsruntime_/api/execution/guardrails/validate"), diff --git a/packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py b/packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py index 16262ca5e..dace18019 100644 --- a/packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py +++ b/packages/uipath-platform/src/uipath/platform/guardrails/guardrails.py @@ -78,6 +78,11 @@ class TextListParameterValue(BaseModel): ] +#: Sentinel ``validator_type`` for Bring Your Own Guardrail (BYOG) guardrails; the +#: connector-backed configuration is referenced by ``byo_validator_name`` instead. +BYO_VALIDATOR_TYPE = "byo" + + class BuiltInValidatorGuardrail(BaseGuardrail): """Built-in validator guardrail model.""" @@ -86,6 +91,7 @@ class BuiltInValidatorGuardrail(BaseGuardrail): validator_parameters: list[ValidatorParameter] = Field( default_factory=list, alias="validatorParameters" ) + byo_validator_name: str | None = Field(default=None, alias="byoValidatorName") model_config = ConfigDict(populate_by_name=True, extra="allow") diff --git a/packages/uipath-platform/tests/services/test_guardrails_service.py b/packages/uipath-platform/tests/services/test_guardrails_service.py index 87fb7bb8c..6fd20bbc5 100644 --- a/packages/uipath-platform/tests/services/test_guardrails_service.py +++ b/packages/uipath-platform/tests/services/test_guardrails_service.py @@ -303,6 +303,173 @@ def capture_request(request): assert result.result == GuardrailValidationResultType.PASSED assert result.reason == "Validation passed" + def test_evaluate_guardrail_byog_forwards_byo_validator_name( + self, + httpx_mock: HTTPXMock, + service: GuardrailsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + """A BYOG guardrail forwards byoValidatorName so the guardrails service + can resolve the connector-backed configuration.""" + captured_request = None + + def capture_request(request): + nonlocal captured_request + captured_request = request + return httpx.Response( + status_code=200, + json={"result": "PASSED", "details": "Validation passed"}, + ) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate", + callback=capture_request, + ) + + # BYOG persisted shape: "byo" sentinel + byoValidatorName reference. + byog_guardrail = BuiltInValidatorGuardrail( + id="byog-id", + name="Databricks PII (BYOG)", + description="Customer-provided PII validator", + enabled_for_evals=True, + selector=GuardrailSelector(scopes=[GuardrailScope.LLM]), + guardrail_type="builtInValidator", + validator_type="byo", + byo_validator_name="my_databricks_pii", + validator_parameters=[], + ) + + result = service.evaluate_guardrail("some input", byog_guardrail) + + assert captured_request is not None + request_payload = json.loads(captured_request.content) + assert request_payload["validator"] == "byo" + assert request_payload["byoValidatorName"] == "my_databricks_pii" + assert result.result == GuardrailValidationResultType.PASSED + + def test_evaluate_guardrail_byo_without_name_raises( + self, + service: GuardrailsService, + ) -> None: + """A "byo" guardrail missing its byoValidatorName reference fails fast + rather than sending an unresolvable request.""" + guardrail = BuiltInValidatorGuardrail( + id="byog-id", + name="Broken BYOG", + enabled_for_evals=True, + selector=GuardrailSelector(scopes=[GuardrailScope.LLM]), + guardrail_type="builtInValidator", + validator_type="byo", + validator_parameters=[], + ) + + with pytest.raises(ValueError, match="byo_validator_name"): + service.evaluate_guardrail("some input", guardrail) + + def test_evaluate_guardrail_byo_validator_name_from_alias(self) -> None: + """byoValidatorName parses into the typed field via its camelCase alias.""" + guardrail = BuiltInValidatorGuardrail.model_validate( + { + "$guardrailType": "builtInValidator", + "id": "byog-id", + "name": "BYOG", + "validatorType": "byo", + "byoValidatorName": "my_databricks_pii", + "validatorParameters": [], + } + ) + assert guardrail.byo_validator_name == "my_databricks_pii" + + def test_evaluate_guardrail_non_byo_type_does_not_forward_name( + self, + httpx_mock: HTTPXMock, + service: GuardrailsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + """byoValidatorName is only forwarded for the "byo" sentinel, never leaked + into a non-BYOG validator payload even if the field happens to be set.""" + captured_request = None + + def capture_request(request): + nonlocal captured_request + captured_request = request + return httpx.Response( + status_code=200, + json={"result": "PASSED", "details": "Validation passed"}, + ) + + httpx_mock.add_callback( + method="POST", + url=f"{base_url}{org}{tenant}/agentsruntime_/api/execution/guardrails/validate", + callback=capture_request, + ) + + guardrail = BuiltInValidatorGuardrail( + id="test-id", + name="PII detection guardrail", + enabled_for_evals=True, + selector=GuardrailSelector(scopes=[GuardrailScope.LLM]), + guardrail_type="builtInValidator", + validator_type="pii_detection", + byo_validator_name="stray_name", + validator_parameters=[], + ) + + service.evaluate_guardrail("some input", guardrail) + + assert captured_request is not None + request_payload = json.loads(captured_request.content) + assert "byoValidatorName" not in request_payload + + def test_evaluate_guardrail_ootb_omits_byo_validator_name( + self, + httpx_mock: HTTPXMock, + service: GuardrailsService, + base_url: str, + org: str, + tenant: str, + ) -> None: + """OOTB validators (no byo_validator_name) keep their payload unchanged — + byoValidatorName is not sent.""" + captured_request = None + + def capture_request(request): + nonlocal captured_request + captured_request = request + return httpx.Response( + status_code=200, + json={"result": "PASSED", "details": "Validation passed"}, + ) + + 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 detection guardrail", + description="Test PII detection", + enabled_for_evals=True, + selector=GuardrailSelector(scopes=[GuardrailScope.LLM]), + guardrail_type="builtInValidator", + validator_type="pii_detection", + validator_parameters=[], + ) + + service.evaluate_guardrail("some input", pii_guardrail) + + assert captured_request is not None + request_payload = json.loads(captured_request.content) + assert "byoValidatorName" not in request_payload + assert pii_guardrail.byo_validator_name is None + def test_evaluate_guardrail_sends_trace_context_headers( self, httpx_mock: HTTPXMock, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 779a6d1e1..802e19ac8 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-07-08T17:52:02.959384Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -1095,7 +1095,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.8" +version = "0.2.9" source = { editable = "." } dependencies = [ { name = "httpx" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index fc5139669..da12487fe 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-07-08T17:52:11.22969Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2741,7 +2741,7 @@ dev = [ [[package]] name = "uipath-platform" -version = "0.2.8" +version = "0.2.9" source = { editable = "../uipath-platform" } dependencies = [ { name = "httpx" },